feat(cleaner-history): add searchBatches DAO method
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,10 @@ import type {
|
|||||||
InsertCleanerExecutionInput,
|
InsertCleanerExecutionInput,
|
||||||
InsertOrderInput,
|
InsertOrderInput,
|
||||||
InsertMaterialDetailInput,
|
InsertMaterialDetailInput,
|
||||||
GetCleanerBatchesOptions
|
GetCleanerBatchesOptions,
|
||||||
|
SearchCleanerHistoryOptions,
|
||||||
|
CleanerSearchBatchResult,
|
||||||
|
CleanerHistorySearchResult
|
||||||
} from '../../types/cleaner-history.types'
|
} from '../../types/cleaner-history.types'
|
||||||
|
|
||||||
const log = createLogger('CleanerOperationHistoryDAO')
|
const log = createLogger('CleanerOperationHistoryDAO')
|
||||||
@@ -980,6 +983,169 @@ export class CleanerOperationHistoryDAO {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== QUERY: SEARCH BATCHES ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-level search across batches, orders, and materials.
|
||||||
|
* Uses a UNION ALL query to find matching BatchIds, then fetches
|
||||||
|
* full nested data for each matched batch.
|
||||||
|
*
|
||||||
|
* @param userId - Optional user ID for filtering
|
||||||
|
* @param options - Search options (query string, usernames filter, result limit)
|
||||||
|
* @returns Search result with matched batches and total count
|
||||||
|
*/
|
||||||
|
async searchBatches(
|
||||||
|
userId: number | undefined,
|
||||||
|
options: SearchCleanerHistoryOptions
|
||||||
|
): Promise<CleanerHistorySearchResult> {
|
||||||
|
const emptyResult: CleanerHistorySearchResult = { batches: [], totalMatches: 0 }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dbService = await this.getDatabaseService()
|
||||||
|
const execTable = this.getExecutionTableName()
|
||||||
|
const orderTable = this.getOrderTableName()
|
||||||
|
const materialTable = this.getMaterialTableName()
|
||||||
|
const dialect = this.getDialect()
|
||||||
|
|
||||||
|
const { query, limit = 20 } = options
|
||||||
|
const safeLimit = Math.floor(limit)
|
||||||
|
const likePattern = `%${query}%`
|
||||||
|
|
||||||
|
// Build UNION ALL to find distinct BatchIds matching the query
|
||||||
|
// Each subquery searches a different table's columns
|
||||||
|
let paramIndex = 0
|
||||||
|
const p = () => dialect.param(paramIndex++)
|
||||||
|
|
||||||
|
const unionSql = `
|
||||||
|
SELECT DISTINCT BatchId FROM (
|
||||||
|
SELECT BatchId FROM ${execTable}
|
||||||
|
WHERE BatchId LIKE ${p()}
|
||||||
|
OR Username LIKE ${p()}
|
||||||
|
OR Status LIKE ${p()}
|
||||||
|
UNION ALL
|
||||||
|
SELECT BatchId FROM ${orderTable}
|
||||||
|
WHERE OrderNumber LIKE ${p()}
|
||||||
|
OR ProductionId LIKE ${p()}
|
||||||
|
UNION ALL
|
||||||
|
SELECT BatchId FROM ${materialTable}
|
||||||
|
WHERE MaterialCode LIKE ${p()}
|
||||||
|
OR MaterialName LIKE ${p()}
|
||||||
|
) AS matched
|
||||||
|
WHERE BatchId IN (
|
||||||
|
SELECT BatchId FROM ${execTable}
|
||||||
|
WHERE 1=1
|
||||||
|
${userId !== undefined ? `AND UserId = ${p()}` : ''}
|
||||||
|
${userId === undefined && options.usernames && options.usernames.length > 0 ? `AND Username IN (${dialect.params(options.usernames.length)})` : ''}
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
const unionParams: (string | number)[] = [
|
||||||
|
likePattern, likePattern, likePattern, // exec table: BatchId, Username, Status
|
||||||
|
likePattern, likePattern, // order table: OrderNumber, ProductionId
|
||||||
|
likePattern, likePattern // material table: MaterialCode, MaterialName
|
||||||
|
]
|
||||||
|
|
||||||
|
// User filtering params
|
||||||
|
if (userId !== undefined) {
|
||||||
|
unionParams.push(userId)
|
||||||
|
} else if (options.usernames && options.usernames.length > 0) {
|
||||||
|
unionParams.push(...options.usernames)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { result } = await trackDuration(
|
||||||
|
async () => await dbService.query(unionSql, unionParams),
|
||||||
|
{
|
||||||
|
operationName: 'CleanerOperationHistoryDAO.searchBatches.union',
|
||||||
|
context: { operationType: 'SELECT', query }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const matchedBatchIds: string[] = result.rows.map((row) => row.BatchId as string)
|
||||||
|
const totalMatches = matchedBatchIds.length
|
||||||
|
const limitedBatchIds = matchedBatchIds.slice(0, safeLimit)
|
||||||
|
|
||||||
|
if (limitedBatchIds.length === 0) {
|
||||||
|
return { batches: [], totalMatches: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch full nested data for each matched batch
|
||||||
|
const batches: CleanerSearchBatchResult[] = []
|
||||||
|
|
||||||
|
for (const batchId of limitedBatchIds) {
|
||||||
|
try {
|
||||||
|
const details = await this.getBatchDetails(batchId)
|
||||||
|
|
||||||
|
if (details.executions.length === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive batch stats from execution records
|
||||||
|
const latestExec = details.executions.reduce((a, b) =>
|
||||||
|
a.attemptNumber > b.attemptNumber ? a : b
|
||||||
|
)
|
||||||
|
|
||||||
|
const batch: CleanerBatchStats = {
|
||||||
|
batchId,
|
||||||
|
userId: latestExec.userId,
|
||||||
|
username: latestExec.username,
|
||||||
|
operationTime: latestExec.operationTime.toISOString(),
|
||||||
|
status: latestExec.status,
|
||||||
|
totalAttempts: details.executions.length,
|
||||||
|
totalOrders: latestExec.totalOrders,
|
||||||
|
ordersProcessed: latestExec.ordersProcessed,
|
||||||
|
totalMaterialsDeleted: latestExec.totalMaterialsDeleted,
|
||||||
|
totalMaterialsFailed: latestExec.totalMaterialsFailed,
|
||||||
|
successCount: details.orders.filter((o) => o.status === 'success').length,
|
||||||
|
failedCount: details.orders.filter((o) => o.status === 'failed').length,
|
||||||
|
isDryRun: latestExec.isDryRun
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch materials for each order
|
||||||
|
const ordersWithMaterials = await Promise.all(
|
||||||
|
details.orders.map(async (order) => {
|
||||||
|
const materials = await this.getMaterialDetails(
|
||||||
|
batchId,
|
||||||
|
order.attemptNumber,
|
||||||
|
order.orderNumber
|
||||||
|
)
|
||||||
|
return { order, materials }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
batches.push({
|
||||||
|
batch,
|
||||||
|
executions: details.executions,
|
||||||
|
orders: ordersWithMaterials
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Error fetching batch data for search result', {
|
||||||
|
operationType: 'SELECT',
|
||||||
|
batchId,
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Search batches completed', {
|
||||||
|
operationType: 'SELECT',
|
||||||
|
requestId: getRequestId(),
|
||||||
|
query,
|
||||||
|
totalMatches,
|
||||||
|
returnedBatches: batches.length
|
||||||
|
})
|
||||||
|
|
||||||
|
return { batches, totalMatches }
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Search batches error', {
|
||||||
|
operationType: 'SELECT',
|
||||||
|
requestId: getRequestId(),
|
||||||
|
query: options.query,
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
})
|
||||||
|
return emptyResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== UTILITIES ====================
|
// ==================== UTILITIES ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user