/
Starolat
/
DeepDive
Обзор
Документация
Войти
/
Starolat
/
DeepDive
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
js/services/DataServiceJsAdapter.js
478 строк
14 KB
Starolat Sergei
Оптимизировать фильтр по периодам через spreadIndexByAssignment
28 июн 2026, 14:54
28 июн 2026, 14:54
8effebd
Код
Авторство
О чём код?
// @ts-check /** * @fileoverview DataServiceJsAdapter — JS-реализация базовых запросов DataService * через DataIndexer-индексы (Phase 4 JS Data Engine). * @module DataServiceJsAdapter * @version 1.0.0 * * Сохраняет публичный API DataService, но выполняет фильтрацию и проекцию * in-memory через DataIndexer (JS Data Engine). */ import dataIndexer from "./DataIndexer.js"; /** * @typedef {import('../types.js').DatabaseIndexes} DatabaseIndexes * @typedef {import('../types.js').Activity} Activity * @typedef {import('../types.js').WBS} WBS * @typedef {import('../types.js').CodeType} CodeType * @typedef {import('../types.js').CodeValue} CodeValue * @typedef {import('../types.js').Project} Project * @typedef {import('../types.js').Resource} Resource * @typedef {import('../types.js').Filter} Filter */ /** * @typedef {Object} ActivityQueryOptions * @property {'current'|'target'|'both'} [scheduleType='both'] * @property {{startIdx: number|null, endIdx: number|null}|null} [weekRange] */ /** * @typedef {Object} WbsQueryOptions * @property {'current'|'target'|'both'} [scheduleType='both'] */ /** * @typedef {Object} CodeQueryOptions * @property {'current'|'target'} [scheduleType='current'] */ const OPERATORS = new Map([ ["Равно", "="], ["equals", "="], ["Не равно", "!="], ["not_equals", "!="], ["Больше", ">"], ["greater", ">"], ["Меньше", "<"], ["less", "<"], ["Больше или равно", ">="], ["greater_or_equal", ">="], ["Меньше или равно", "<="], ["less_or_equal", "<="], ["Содержит", "contains"], ["contains", "contains"], ["Начинается с", "starts_with"], ["starts_with", "starts_with"], ["Заканчивается на", "ends_with"], ["ends_with", "ends_with"], ["Между", "between"], ["between", "between"], ["В списке", "in"], ["in", "in"], ]); class DataServiceJsAdapter { /** * Получение индексов по ID базы. * @private * @param {string} databaseId * @returns {DatabaseIndexes|null} */ _getIndexes(databaseId) { return dataIndexer.getIndexes(databaseId); } /** * Получение списка проектов. * @param {string} databaseId * @returns {Promise<Project[]>} */ async getProjects(databaseId) { const indexes = this._getIndexes(databaseId); if (!indexes) return []; return Array.from(indexes.projectsById.values()).sort((a, b) => String(a.shortName || a.name).localeCompare( String(b.shortName || b.name), ), ); } /** * Получение работ с фильтрацией. * @param {string} databaseId * @param {Filter[]} [filters] * @param {ActivityQueryOptions} [options] * @returns {Promise<Activity[]>} */ async getActivities(databaseId, filters = [], options = {}) { const indexes = this._getIndexes(databaseId); if (!indexes) return []; const scheduleType = options.scheduleType || "both"; let activities = this._getActivitiesBySchedule(indexes, scheduleType); if (options.weekRange) { activities = activities.filter((a) => this._activityMatchesWeekRange(a, indexes, options.weekRange), ); } if (filters && filters.length > 0) { activities = activities.filter((a) => this._activityMatchesFilters(a, filters), ); } // Сортировка по id, затем scheduleType для parity с SQL activities.sort((a, b) => { if (a.id !== b.id) return a.id - b.id; if (a.scheduleType === b.scheduleType) return 0; return a.scheduleType === "current" ? -1 : 1; }); return activities; } /** * Получение WBS. * @param {string} databaseId * @param {WbsQueryOptions} [options] * @returns {Promise<WBS[]>} */ async getWBS(databaseId, options = {}) { const indexes = this._getIndexes(databaseId); if (!indexes) return []; const scheduleType = options.scheduleType || "both"; let rows = Array.from(indexes.wbsById.values()); if (scheduleType !== "both") { rows = rows.filter((w) => w.scheduleType === scheduleType); } rows.sort((a, b) => { const seqDiff = (a.seqNum || 0) - (b.seqNum || 0); if (seqDiff !== 0) return seqDiff; return String(a.code || "").localeCompare(String(b.code || "")); }); return rows; } /** * Получение типов кодов, используемых в назначениях. * @param {string} databaseId * @param {CodeQueryOptions} [options] * @returns {Promise<CodeType[]>} */ async getCodeTypes(databaseId, options = {}) { const indexes = this._getIndexes(databaseId); if (!indexes) return []; const scheduleType = options.scheduleType || "current"; const usedTypeIds = this._getUsedCodeTypeIds(indexes, scheduleType); const rows = Array.from(indexes.codeTypeById.values()) .filter((ct) => usedTypeIds.has(ct.typeId)) .sort((a, b) => String(a.name).localeCompare(String(b.name))); return rows; } /** * Получение значений кода для типа. * @param {string} databaseId * @param {string|number} codeTypeId * @param {CodeQueryOptions} [options] * @returns {Promise<CodeValue[]>} */ async getCodeValues(databaseId, codeTypeId, options = {}) { const indexes = this._getIndexes(databaseId); if (!indexes) return []; const typeId = Number(codeTypeId); const scheduleType = options.scheduleType || "current"; const byType = indexes.codeValuesByType.get(typeId); if (!byType) return []; const usedValueIds = this._getUsedCodeValueIds(indexes, typeId, scheduleType); const rows = Array.from(byType.values()) .filter((cv) => usedValueIds.has(cv.valueId)) .sort((a, b) => { const orderDiff = (a.sortOrder || 0) - (b.sortOrder || 0); if (orderDiff !== 0) return orderDiff; return String(a.shortName || "").localeCompare(String(b.shortName || "")); }); return rows; } /** * Получение всех ресурсов. * @param {string} databaseId * @returns {Promise<Resource[]>} */ async getResources(databaseId) { const indexes = this._getIndexes(databaseId); if (!indexes) return []; return Array.from(indexes.resourcesById.values()).sort((a, b) => String(a.name).localeCompare(String(b.name)), ); } /** * Получение материальных ресурсов. * @param {string} databaseId * @returns {Promise<Resource[]>} */ async getMaterialResources(databaseId) { const indexes = this._getIndexes(databaseId); if (!indexes) return []; return Array.from(indexes.resourcesById.values()) .filter((r) => r.type === "material") .sort((a, b) => String(a.name).localeCompare(String(b.name))); } // ========================================================================= // Helpers // ========================================================================= /** * @private * @param {DatabaseIndexes} indexes * @param {'current'|'target'|'both'} scheduleType * @returns {Activity[]} */ _getActivitiesBySchedule(indexes, scheduleType) { if (scheduleType === "both") { return Array.from(indexes.activitiesById.values()); } const bySchedule = indexes.activitiesBySchedule.get(scheduleType); if (!bySchedule) return []; return Array.from(bySchedule.values()); } /** * @private * @param {Activity} activity * @param {DatabaseIndexes} indexes * @param {{startIdx: number|null, endIdx: number|null}} weekRange * @returns {boolean} */ _activityMatchesWeekRange(activity, indexes, weekRange) { const assignments = indexes.assignmentsByActivity.get(activity.id); if (!assignments || assignments.length === 0) return false; const startDefined = weekRange.startIdx !== null && weekRange.startIdx !== undefined; const endDefined = weekRange.endIdx !== null && weekRange.endIdx !== undefined; if (!startDefined && !endDefined) return true; const startValid = startDefined && Number.isFinite(Number(weekRange.startIdx)); const endValid = endDefined && Number.isFinite(Number(weekRange.endIdx)); if ((startDefined && !startValid) || (endDefined && !endValid)) { return true; } const startIdx = startValid ? Number(weekRange.startIdx) : -Infinity; const endIdx = endValid ? Number(weekRange.endIdx) : Infinity; const spread = indexes.spread; const spreadIndexByAssignment = indexes.spreadIndexByAssignment; for (const ra of assignments) { const assignmentId = ra.assignmentId; if (assignmentId == null) continue; const range = spreadIndexByAssignment.get(assignmentId); if (!range) continue; for (let i = range.start; i < range.end; i++) { const wi = spread.weekIndices[i]; if (wi >= startIdx && wi <= endIdx) return true; } } return false; } /** * @private * @param {Activity} activity * @param {Filter[]} filters * @returns {boolean} */ _activityMatchesFilters(activity, filters) { for (const filter of filters) { if (!filter || !filter.field) continue; if (!this._evaluateFilter(activity, filter)) return false; } return true; } /** * @private * @param {Activity} activity * @param {Filter} filter * @returns {boolean} */ _evaluateFilter(activity, filter) { const op = OPERATORS.get(filter.condition); if (!op) { // Unknown condition — treat as equality for backward compat. return String(this._getField(activity, filter.field)) === String(filter.value); } const value = this._getField(activity, filter.field); switch (op) { case "=": return value === filter.value; case "!=": return value !== filter.value; case ">": return value != null && filter.value != null && value > filter.value; case "<": return value != null && filter.value != null && value < filter.value; case ">=": return value != null && filter.value != null && value >= filter.value; case "<=": return value != null && filter.value != null && value <= filter.value; case "contains": return ( value != null && String(value).toLowerCase().includes(String(filter.value).toLowerCase()) ); case "starts_with": return ( value != null && String(value).toLowerCase().startsWith(String(filter.value).toLowerCase()) ); case "ends_with": return ( value != null && String(value).toLowerCase().endsWith(String(filter.value).toLowerCase()) ); case "between": { const arr = Array.isArray(filter.value) ? filter.value : String(filter.value).split(",").map((s) => s.trim()); if (arr.length !== 2) return false; return value != null && value >= arr[0] && value <= arr[1]; } case "in": { const arr = Array.isArray(filter.value) ? filter.value : [filter.value]; return arr.some((v) => v === value); } default: return value === filter.value; } } /** * @private * @param {Activity} activity * @param {string} field * @returns {any} */ _getField(activity, field) { switch (field) { case "id": case "activity_id": return activity.id; case "scheduleType": case "schedule_type": return activity.scheduleType; case "projectId": case "project_id": return activity.projectId; case "wbsId": case "wbs_id": return activity.wbsId; case "activityCode": case "activity_code": return activity.activityCode; case "taskName": case "task_name": return activity.taskName; case "taskType": case "task_type": return activity.taskType; case "duration": return activity.duration; case "totalFloat": case "total_float": return activity.totalFloat; case "freeFloat": case "free_float": return activity.freeFloat; case "startDate": case "start_date": return activity.startDate; case "endDate": case "end_date": return activity.endDate; case "actualStartDate": case "actual_start_date": return activity.actualStartDate; case "actualEndDate": case "actual_end_date": return activity.actualEndDate; case "totalManhours": case "total_manhours": return activity.totalManhours; case "actualManhours": case "actual_manhours": return activity.actualManhours; case "remainingManhours": case "remaining_manhours": return activity.remainingManhours; default: return activity[/** @type {keyof Activity} */ (field)]; } } /** * @private * @param {DatabaseIndexes} indexes * @param {'current'|'target'} scheduleType * @returns {Set<number>} */ _getUsedCodeTypeIds(indexes, scheduleType) { const used = new Set(); const activitiesBySchedule = indexes.activitiesBySchedule.get(scheduleType); const activityIds = activitiesBySchedule ? new Set(activitiesBySchedule.keys()) : new Set(indexes.activitiesById.keys()); for (const [activityId, typeMap] of indexes.activityCodesByType) { if (activityIds.has(activityId)) { for (const typeId of typeMap.keys()) { used.add(typeId); } } } return used; } /** * @private * @param {DatabaseIndexes} indexes * @param {number} typeId * @param {'current'|'target'} scheduleType * @returns {Set<number>} */ _getUsedCodeValueIds(indexes, typeId, scheduleType) { const used = new Set(); const activitiesBySchedule = indexes.activitiesBySchedule.get(scheduleType); const activityIds = activitiesBySchedule ? new Set(activitiesBySchedule.keys()) : new Set(indexes.activitiesById.keys()); for (const [activityId, typeMap] of indexes.activityCodesByType) { if (!activityIds.has(activityId)) continue; const valueId = typeMap.get(typeId); if (valueId != null) { used.add(valueId); } } return used; } } const dataServiceJsAdapter = new DataServiceJsAdapter(); export { dataServiceJsAdapter, DataServiceJsAdapter }; export default dataServiceJsAdapter;