fix(lint): resolve all ESLint warnings and errors in Next.js frontend

- next.config.ts: remove non-null assertions, type webpack param properly
- bootstrap/route.ts: simplify null check to != null
- workflows/route.ts: fix nullable string conditional, remove unnecessary ??
- ExecutionMonitor.tsx: add braces to void-returning arrow functions
- WorkflowBuilder.tsx: explicit null checks for nullable objects
- package-utils.ts: remove unnecessary ??, explicit null check
- fetch-session.ts, login.ts, get-current-user.ts: remove unnecessary ?. chains
- db-client.ts: explicit null checks
- error-reporting.ts: type ErrorCategory properly
- multi-tenant-context.examples.ts: remove await of non-Promise, prefer-optional-chain
- multi-tenant-context.ts: nullable boolean ==>  === true, remove unused eslint-disable,
  remove async from bindCredentials, restore executionLimits fallback to getDefaultExecutionLimits()
- workflow-error-handler.ts: remove unnecessary ?? fallbacks
- workflow-loader-v2.ts: remove unused eslint-disable, use ??= assignment
- store.ts: remove unnecessary type assertions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 23:16:28 +00:00
parent 141d4333df
commit fb0a3fd9cf
16 changed files with 95 additions and 104 deletions

View File

@@ -1,5 +1,6 @@
import type { NextConfig } from 'next'
import type { Configuration } from 'webpack'
import type webpack from 'webpack'
import path from 'path'
const projectDir = process.cwd()
@@ -101,12 +102,13 @@ const nextConfig: NextConfig = {
'@dbal-ui': path.resolve(projectDir, '../../dbal/shared/ui'),
},
},
webpack(config: Configuration, { isServer, webpack }) {
webpack(config: Configuration, { isServer, webpack: wp }: { isServer: boolean; webpack: typeof webpack }) {
// Stub ALL external SCSS module imports with an actual .module.scss
// so they go through the CSS module pipeline (css-loader sets .default correctly)
const stubScss = path.resolve(projectDir, 'src/lib/empty.module.scss')
config.plugins!.push(
new webpack.NormalModuleReplacementPlugin(
config.plugins ??= []
config.plugins.push(
new wp.NormalModuleReplacementPlugin(
/\.module\.scss$/,
function (resource: { context?: string; request?: string }) {
const ctx = resource.context ?? ''
@@ -116,30 +118,36 @@ const nextConfig: NextConfig = {
}
)
)
config.optimization!.minimize = false
config.resolve!.alias = {
...(config.resolve!.alias as Record<string, string>),
'@metabuilder/components': path.resolve(projectDir, 'src/lib/components-shim.ts'),
'@dbal-ui': path.resolve(projectDir, '../../dbal/shared/ui'),
// Resolve service-adapters to source (dist/ is not pre-built)
'@metabuilder/service-adapters': path.resolve(monorepoRoot, 'redux/adapters/src'),
if (config.optimization != null) {
config.optimization.minimize = false
}
config.externals = [...((config.externals as string[]) ?? []), 'esbuild']
if (config.resolve != null) {
config.resolve.alias = {
...(config.resolve.alias as Record<string, string>),
'@metabuilder/components': path.resolve(projectDir, 'src/lib/components-shim.ts'),
'@dbal-ui': path.resolve(projectDir, '../../dbal/shared/ui'),
// Resolve service-adapters to source (dist/ is not pre-built)
'@metabuilder/service-adapters': path.resolve(monorepoRoot, 'redux/adapters/src'),
}
}
config.externals = [...(config.externals as string[]), 'esbuild']
if (!isServer) {
config.resolve!.fallback = {
...(config.resolve!.fallback as Record<string, false>),
'@aws-sdk/client-s3': false,
fs: false,
path: false,
crypto: false,
stream: false,
'stream/promises': false,
os: false,
buffer: false,
util: false,
if (config.resolve != null) {
config.resolve.fallback = {
...(config.resolve.fallback as Record<string, false>),
'@aws-sdk/client-s3': false,
fs: false,
path: false,
crypto: false,
stream: false,
'stream/promises': false,
os: false,
buffer: false,
util: false,
}
}
}

View File

@@ -70,7 +70,7 @@ async function dbalPost(entity: string, data: Record<string, unknown>): Promise<
export async function POST(request: NextRequest) {
const limitResponse = applyRateLimit(request, 'bootstrap')
if (limitResponse !== null && limitResponse !== undefined) {
if (limitResponse != null) {
return limitResponse
}

View File

@@ -117,9 +117,8 @@ export async function GET(
const offset = parseInt(searchParams.get('offset') ?? '0')
const category = searchParams.get('category')
const tags = searchParams.get('tags')?.split(',').map((t) => t.trim())
const active = searchParams.get('active')
? searchParams.get('active') === 'true'
: undefined
const activeParam = searchParams.get('active')
const active = activeParam != null ? activeParam === 'true' : undefined
// 6. Build filter
const filter: Record<string, unknown> = {
@@ -286,9 +285,9 @@ export async function POST(
notificationChannels: [],
},
nodes: Array.isArray(body.nodes) ? (body.nodes as unknown[]) : [],
connections: (body.connections as Record<string, unknown>) ?? {},
connections: (body.connections as Record<string, unknown>),
triggers: Array.isArray(body.triggers) ? (body.triggers as unknown[]) : [],
variables: (body.variables as Record<string, unknown>) ?? {},
variables: (body.variables as Record<string, unknown>),
errorHandling: {
default: 'stopWorkflow' as const,
errorNotification: false,
@@ -309,7 +308,7 @@ export async function POST(
onLimitExceeded: 'reject' as const,
},
credentials: [],
metadata: (body.metadata as Record<string, unknown>) ?? {},
metadata: (body.metadata as Record<string, unknown>),
executionLimits: {
maxExecutionTime: 300000,
maxMemoryMb: 512,

View File

@@ -121,7 +121,7 @@ export const ExecutionMonitor: React.FC<ExecutionMonitorProps> = ({
key={execution.id}
execution={execution}
isSelected={selectedExecutionId === execution.id}
onClick={() => handleExecutionSelect(execution.id)}
onClick={() => { handleExecutionSelect(execution.id) }}
/>
))}
@@ -146,11 +146,11 @@ export const ExecutionMonitor: React.FC<ExecutionMonitorProps> = ({
nodeId={nodeId}
result={result}
isExpanded={expandedNodeId === nodeId}
onToggle={() =>
onToggle={() => {
setExpandedNodeId(
expandedNodeId === nodeId ? null : nodeId
)
}
}}
/>
))}
</div>
@@ -420,7 +420,7 @@ const LogViewer: React.FC<LogViewerProps> = ({ logs }) => {
<button
key={level}
className={`${styles.filterButton} ${filter === level ? styles.active : ''}`}
onClick={() => setFilter(level)}
onClick={() => { setFilter(level) }}
>
{level}
</button>

View File

@@ -167,7 +167,7 @@ export const WorkflowBuilder: React.FC<WorkflowBuilderProps> = ({
<input
type="text"
placeholder={variable.description ?? name}
value={(triggerData[name] as string) ?? ''}
value={(triggerData[name] as string)}
onChange={(e) => {
handleTriggerDataChange(name, e.target.value)
}}
@@ -344,13 +344,13 @@ function renderConnections(workflow: WorkflowDefinition) {
Object.entries(workflow.connections).forEach(([fromNodeId, portMap]: [string, Record<string, Record<string, ConnectionTarget[]>>]) => {
const fromNode = workflow.nodes.find((n) => n.id === fromNodeId)
if (!fromNode) return
if (fromNode == null) return
Object.entries(portMap).forEach(([_portName, indexMap]: [string, Record<string, ConnectionTarget[]>]) => {
Object.entries(indexMap).forEach(([_, targets]: [string, ConnectionTarget[]]) => {
targets.forEach((target: ConnectionTarget) => {
const toNode = workflow.nodes.find((n) => n.id === target.node)
if (!toNode) return
if (toNode == null) return
const [x1, y1] = fromNode.position
const [x2, y2] = toNode.position

View File

@@ -82,7 +82,7 @@ export function getErrorMessage(error: PackageError | Error | null): string {
return messages[error.code] ?? error.message
}
return error.message ?? 'An unknown error occurred'
return error.message
}
/**
@@ -135,7 +135,7 @@ export function formatVersion(version: string): string {
// Ensure version matches semver format
const semverRegex = /^\d+\.\d+\.\d+/
const match = version.match(semverRegex)
return match ? match[0] : version
return match != null ? match[0] : version
}
/**

View File

@@ -28,7 +28,7 @@ export async function fetchSession(): Promise<User | null> {
filter: { token: sessionToken }
})
const session = sessions.data?.[0] as DbalSessionRecord | undefined
const session = sessions.data[0] as DbalSessionRecord | undefined
if (session === undefined) {
return null

View File

@@ -45,7 +45,7 @@ export async function login(identifier: string, password: string): Promise<Login
}
})
let user = users.data?.[0] as DbalUserRecord | undefined
let user = users.data[0] as DbalUserRecord | undefined
// If not found by username, try email
if (user === undefined) {
@@ -54,7 +54,7 @@ export async function login(identifier: string, password: string): Promise<Login
email: identifier
}
})
user = usersByEmail.data?.[0] as DbalUserRecord | undefined
user = usersByEmail.data[0] as DbalUserRecord | undefined
}
if (user === undefined) {

View File

@@ -26,7 +26,7 @@ export async function getCurrentUser(): Promise<CurrentUser | null> {
const cookieStore = await cookies()
const sessionToken = cookieStore.get(SESSION_COOKIE)
if (!sessionToken?.value || sessionToken.value.length === 0) {
if (sessionToken?.value == null || sessionToken.value.length === 0) {
return null
}
@@ -35,7 +35,7 @@ export async function getCurrentUser(): Promise<CurrentUser | null> {
filter: { token: sessionToken.value }
})
const session = sessions.data?.[0] as DbalSessionRecord | undefined
const session = sessions.data[0] as DbalSessionRecord | undefined
if (session == null) {
return null

View File

@@ -109,7 +109,7 @@ function createOps(entityName: string): EntityOps {
return {
async list(options?: ListOptions): Promise<ListResult> {
const params = new URLSearchParams()
if (options?.filter) {
if (options?.filter != null) {
for (const [k, v] of Object.entries(options.filter)) {
if (v !== undefined && v !== null) params.set(k, String(v as string | number | boolean))
}
@@ -127,7 +127,7 @@ function createOps(entityName: string): EntityOps {
if (Array.isArray(payload)) {
return { data: payload, total: payload.length }
}
if (payload != null && Array.isArray(payload.data)) {
if (Array.isArray(payload.data)) {
return { data: payload.data as Record<string, unknown>[], total: payload.total as number | undefined }
}
return { data: [] }

View File

@@ -161,7 +161,7 @@ class ErrorReportingService {
// suggestedAction reflects the current category, even if mutated after creation
Object.defineProperty(report, 'suggestedAction', {
get() { return getSuggestedAction(this.category) },
get(this: ErrorReport) { return getSuggestedAction(this.category) },
enumerable: true,
configurable: true,
})

View File

@@ -332,7 +332,7 @@ export async function executeScheduledWorkflow(
})
// 2. Get schedule configuration
const trigger = workflow.triggers?.find((t) => t.kind === 'schedule')
const trigger = workflow.triggers.find((t) => t.kind === 'schedule')
const cronExpression = trigger?.schedule ?? '0 */6 * * *'
const context = await builder.build(
@@ -430,7 +430,7 @@ export async function validateWorkflowExecution(req: NextRequest) {
}
const builder = new MultiTenantContextBuilder(workflow, requestContext)
const result = await builder.validate()
const result = builder.validate()
// Return validation result for UI
return NextResponse.json({
@@ -559,8 +559,8 @@ export async function logExecutionContext(context: ExtendedWorkflowContext) {
workflow: sanitized.workflowId,
tenant: sanitized.tenantId,
user: sanitized.userId,
mode: (sanitized.multiTenant as Record<string, unknown>)?.executionMode,
timestamp: (sanitized.multiTenant as Record<string, unknown>)?.requestedAt,
mode: (sanitized.multiTenant as Record<string, unknown>).executionMode,
timestamp: (sanitized.multiTenant as Record<string, unknown>).requestedAt,
// Variables listed as keys only, no values
variables: sanitized.variables,
// Limits included for monitoring
@@ -657,7 +657,7 @@ export async function retryFailedWorkflowExecution(
async function verifyUserAuth(req: NextRequest): Promise<AuthenticatedUser | null> {
// In production, parse JWT and get user details
const authHeader = req.headers.get('authorization')
if (authHeader === null || !authHeader.startsWith('Bearer ')) {
if (authHeader?.startsWith('Bearer ') !== true) {
return null
}

View File

@@ -189,7 +189,7 @@ export class MultiTenantContextBuilder {
triggerData: requestData?.triggerData ?? {},
variables: this.buildVariables(requestData?.variables),
secrets: requestData?.secrets ?? {},
request: this.options.captureRequestData ? requestData?.request as WorkflowContext['request'] : undefined,
request: this.options.captureRequestData === true ? requestData?.request as WorkflowContext['request'] : undefined,
multiTenant: multiTenantMeta,
requestMetadata: {
ipAddress: this.requestContext.ipAddress,
@@ -205,15 +205,15 @@ export class MultiTenantContextBuilder {
this.validateContextSafety(context)
// 5. Load and bind credentials
if (this.options.enforceCredentialValidation) {
await this.bindCredentials(context)
if (this.options.enforceCredentialValidation === true) {
this.bindCredentials(context)
}
// 6. Validate variables don't cross tenants
this.validateVariableTenantIsolation(context)
// 7. Log context creation (audit)
if (this.options.enableAuditLogging) {
if (this.options.enableAuditLogging === true) {
this.logContextCreation(context)
}
@@ -231,13 +231,12 @@ export class MultiTenantContextBuilder {
// Super-admin (level 4) can access any tenant
if (this.requestContext.userLevel >= 4) {
if (!this.options.allowCrossTenantAccess) {
if (this.options.allowCrossTenantAccess !== true) {
throw new Error(
`Cross-tenant access disabled: User ${this.requestContext.userId} ` +
`cannot access workflow in tenant ${this.workflow.tenantId}`
)
}
// eslint-disable-next-line no-console
console.warn(
`[SECURITY] Super-admin ${this.requestContext.userId} accessing ` +
`cross-tenant workflow ${this.workflow.id}`
@@ -327,28 +326,24 @@ export class MultiTenantContextBuilder {
const variables: DataRecord = {}
// 1. Add workflow defaults
if (this.workflow.variables != null) {
for (const [varName, varDef] of Object.entries(this.workflow.variables)) {
// Only allow workflow and execution scopes (not global)
if (varDef.scope === 'global') {
// eslint-disable-next-line no-console
console.warn(`[SECURITY] Skipping global-scope variable ${varName} - not allowed`)
continue
}
variables[varName] = varDef.defaultValue ?? null
for (const [varName, varDef] of Object.entries(this.workflow.variables)) {
// Only allow workflow and execution scopes (not global)
if (varDef.scope === 'global') {
console.warn(`[SECURITY] Skipping global-scope variable ${varName} - not allowed`)
continue
}
variables[varName] = varDef.defaultValue ?? null
}
// 2. Merge request overrides
if (requestVariables != null) {
for (const [varName, varValue] of Object.entries(requestVariables)) {
// Validate variable is allowed by workflow
const varDef = this.workflow.variables?.[varName]
const varDef = this.workflow.variables[varName]
if (varDef != null) {
variables[varName] = varValue
} else {
// eslint-disable-next-line no-console
console.warn(
`[SECURITY] Rejecting unknown variable ${varName} - not in workflow definition`
)
@@ -371,7 +366,7 @@ export class MultiTenantContextBuilder {
const errors: string[] = []
// 1. Tenant ID must match (unless cross-tenant access is explicitly allowed)
if (context.tenantId !== this.workflow.tenantId && !this.options.allowCrossTenantAccess) {
if (context.tenantId !== this.workflow.tenantId && this.options.allowCrossTenantAccess !== true) {
errors.push(
`Context tenant ${context.tenantId} does not match ` +
`workflow tenant ${this.workflow.tenantId}`
@@ -389,13 +384,12 @@ export class MultiTenantContextBuilder {
}
// 4. Check execution limits
if (this.workflow.executionLimits != null) {
if (context.executionLimits.maxExecutionTime > this.workflow.executionLimits.maxExecutionTime) {
errors.push(
`Requested execution time (${String(context.executionLimits.maxExecutionTime)}ms) ` +
`exceeds workflow limit (${String(this.workflow.executionLimits.maxExecutionTime)}ms)`
)
}
const workflowLimit = this.workflow.executionLimits ?? this.getDefaultExecutionLimits()
if (context.executionLimits.maxExecutionTime > workflowLimit.maxExecutionTime) {
errors.push(
`Requested execution time (${String(context.executionLimits.maxExecutionTime)}ms) ` +
`exceeds workflow limit (${String(workflowLimit.maxExecutionTime)}ms)`
)
}
if (errors.length > 0) {
@@ -425,8 +419,8 @@ export class MultiTenantContextBuilder {
/**
* Load and bind credentials from workflow definition
*/
private async bindCredentials(context: ExtendedWorkflowContext): Promise<void> {
const bindings = this.workflow.credentials ?? []
private bindCredentials(context: ExtendedWorkflowContext): void {
const bindings = this.workflow.credentials
for (const binding of bindings) {
try {
@@ -449,7 +443,6 @@ export class MultiTenantContextBuilder {
name: binding.credentialName,
})
} catch (error: unknown) {
// eslint-disable-next-line no-console
console.error(`Failed to bind credential for node ${binding.nodeId}:`, error)
throw error
}
@@ -535,7 +528,7 @@ export class MultiTenantContextBuilder {
}
// 4. Check for global scope variables
for (const [varName, varDef] of Object.entries(this.workflow.variables ?? {})) {
for (const [varName, varDef] of Object.entries(this.workflow.variables)) {
if (varDef.scope === 'global') {
warnings.push({
path: `variables.${varName}`,
@@ -546,8 +539,8 @@ export class MultiTenantContextBuilder {
}
// 5. Check credentials
const credentialCount = this.workflow.credentials?.length ?? 0
if (this.options.enforceCredentialValidation && credentialCount > 0) {
const credentialCount = this.workflow.credentials.length
if (this.options.enforceCredentialValidation === true && credentialCount > 0) {
warnings.push({
path: 'credentials',
message: `${String(credentialCount)} credential(s) will be validated during execution`,

View File

@@ -387,9 +387,9 @@ export class WorkflowErrorHandler {
): WorkflowApiResponse {
const code = this.getErrorCode(error)
const message = this.isDevelopment
? (this.getErrorMessage(error) ?? ERROR_MESSAGES[code])
: (ERROR_MESSAGES[code] ?? this.getErrorMessage(error))
const statusCode = ERROR_STATUS_MAP[code] ?? 500
? this.getErrorMessage(error)
: ERROR_MESSAGES[code]
const statusCode = ERROR_STATUS_MAP[code]
const response: FormattedError = {
success: false,
@@ -462,7 +462,7 @@ export class WorkflowErrorHandler {
errorCode: WorkflowErrorCode,
context: ErrorContext = {}
): WorkflowApiResponse {
const statusCode = ERROR_STATUS_MAP[errorCode] ?? 401
const statusCode = ERROR_STATUS_MAP[errorCode]
const response: FormattedError = {
success: false,
@@ -636,7 +636,7 @@ export class WorkflowErrorHandler {
* Get suggestion for validation error
*/
private getSuggestionForError(error: ValidationError): string {
const code = (error.code ?? '').toUpperCase()
const code = error.code.toUpperCase()
const suggestions: Record<string, string> = {
MISSING_REQUIRED_FIELD: 'Add the missing parameter to the node.',
INVALID_NODE_TYPE: 'Use a valid node type from the registry.',

View File

@@ -282,7 +282,6 @@ export class ValidationCache {
}
if (cleaned > 0) {
// eslint-disable-next-line no-console
console.warn(`[CACHE CLEANUP] Removed ${cleaned} expired entries`)
}
}, 5 * 60 * 1000) // Every 5 minutes
@@ -416,7 +415,6 @@ export class WorkflowLoaderV2 {
const cached = this.cache.get(cacheKey)
if (cached != null) {
if (this.enableLogging) {
// eslint-disable-next-line no-console
console.warn(`[CACHE HIT] Validation for workflow ${workflow.id}`)
}
return {
@@ -430,7 +428,6 @@ export class WorkflowLoaderV2 {
const existingValidation = this.activeValidations.get(validationKey)
if (existingValidation != null) {
if (this.enableLogging) {
// eslint-disable-next-line no-console
console.warn(
`[DEDUP] Reusing in-flight validation for ${validationKey}`
)
@@ -475,7 +472,6 @@ export class WorkflowLoaderV2 {
workflows: WorkflowDefinition[]
): Promise<ExtendedValidationResult[]> {
if (this.enableLogging) {
// eslint-disable-next-line no-console
console.warn(`Starting batch validation for ${workflows.length} workflows`)
}
@@ -541,7 +537,6 @@ export class WorkflowLoaderV2 {
const cacheKey = `${tenantId}:${workflowId}`
this.cache.delete(cacheKey)
if (this.enableLogging) {
// eslint-disable-next-line no-console
console.warn(`[CACHE INVALIDATED] ${workflowId}`)
}
}
@@ -596,7 +591,6 @@ export class WorkflowLoaderV2 {
clearCache(): void {
this.cache.clear()
if (this.enableLogging) {
// eslint-disable-next-line no-console
console.warn('All validation caches cleared')
}
}
@@ -662,7 +656,6 @@ export class WorkflowLoaderV2 {
const duration = Date.now() - startTime
if (this.enableLogging) {
// eslint-disable-next-line no-console
console.warn(`[VALIDATION] Workflow ${workflow.id} validated in ${duration}ms`, {
nodeCount: workflow.nodes.length,
connectionCount: Object.keys(workflow.connections).length,
@@ -869,9 +862,7 @@ let globalLoader: WorkflowLoaderV2 | null = null
export function getWorkflowLoader(
options?: WorkflowLoaderV2Options
): WorkflowLoaderV2 {
if (globalLoader == null) {
globalLoader = new WorkflowLoaderV2(options)
}
globalLoader ??= new WorkflowLoaderV2(options)
return globalLoader
}

View File

@@ -46,14 +46,14 @@ const { store, persistor } = createPersistedStore({
middleware: (base: Middleware[]) => {
const middlewares: Middleware[] = [...base]
if (isDev) {
middlewares.push(createLoggingMiddleware({ verbose: false }) as Middleware)
middlewares.push(createPerformanceMiddleware() as Middleware)
middlewares.push(createLoggingMiddleware({ verbose: false }))
middlewares.push(createPerformanceMiddleware())
}
middlewares.push(createAnalyticsMiddleware() as Middleware)
middlewares.push(createErrorMiddleware() as Middleware)
middlewares.push(createAnalyticsMiddleware())
middlewares.push(createErrorMiddleware())
return middlewares
},
devTools: getDevToolsConfig(),
devTools: getDevToolsConfig() as boolean | object,
ignoredActions: ['asyncData/fetchAsyncData/pending'],
ignoredPaths: ['asyncData.requests.*.promise'],
})