Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
787d106
create sequelize repositories (data layer)
mariscalromeroalejandro Sep 25, 2025
fa152aa
object options enum
mariscalromeroalejandro Sep 30, 2025
438a18e
support for seeds
mariscalromeroalejandro Sep 30, 2025
13db24b
update repositories and their tests
mariscalromeroalejandro Sep 30, 2025
9fd1371
mapper to patch a layout
mariscalromeroalejandro Sep 30, 2025
937ed45
layout service helpers to keep the database synchronized
mariscalromeroalejandro Sep 30, 2025
ab5a444
move bookkeeping service to external folder
mariscalromeroalejandro Sep 30, 2025
813fee5
use services to interact with the repositories
mariscalromeroalejandro Sep 30, 2025
f97b685
update controllers and middlewares
mariscalromeroalejandro Oct 1, 2025
cd25dad
database configuration logic & refacotr api and QCModel
mariscalromeroalejandro Oct 1, 2025
6e4de56
helpers tests
mariscalromeroalejandro Oct 1, 2025
a3e4f05
fix controller tests
mariscalromeroalejandro Oct 1, 2025
fe5f5d7
modify migration to make layout names unique
mariscalromeroalejandro Oct 1, 2025
6885e53
fix lint issues
mariscalromeroalejandro Oct 1, 2025
3e2bf5e
Refactor layoutIdMiddleware to accept LayoutService
mariscalromeroalejandro Oct 3, 2025
08180b0
Adapt LayoutController to use req.layout and new getLayoutByName
mariscalromeroalejandro Oct 3, 2025
f7be900
filter layouts by object path
mariscalromeroalejandro Oct 3, 2025
c016871
return single object in findObjectByChartId
mariscalromeroalejandro Oct 3, 2025
db307f4
fix layoutDisplayOptions retrieval
mariscalromeroalejandro Oct 3, 2025
6691362
handle non-JSON responses gracefully
mariscalromeroalejandro Oct 3, 2025
02ee55d
fix tests
mariscalromeroalejandro Oct 3, 2025
0c9d9db
fix superfluous trailing arguments warning
mariscalromeroalejandro Oct 3, 2025
c1d5d90
add service_healthy dependency to wait until the database service ha…
mariscalromeroalejandro Oct 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion QualityControl/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ RUN apk add --no-cache \
freetype=2.13.2-r0 \
freetype-dev=2.13.2-r0 \
harfbuzz=8.5.0-r0 \
ca-certificates=20250619-r0
ca-certificates=20250911-r0

ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
Expand Down
30 changes: 30 additions & 0 deletions QualityControl/common/library/enums/objectOptions.enum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const ObjectOption = {
lego: 'lego',
colz: 'colz',
lcolz: 'lcolz',
text: 'text',
logx: 'logx',
logy: 'logy',
logz: 'logz',
gridx: 'gridx',
gridy: 'gridy',
gridz: 'gridz',
stat: 'stat',
};

export const drawingOptions = [
ObjectOption.lego,
ObjectOption.colz,
ObjectOption.lcolz,
ObjectOption.text,
];

export const displayHints = [
ObjectOption.logx,
ObjectOption.logy,
ObjectOption.logz,
ObjectOption.gridx,
ObjectOption.gridy,
ObjectOption.gridz,
ObjectOption.stat,
];
2 changes: 2 additions & 0 deletions QualityControl/config-default.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export const config = {
timezone: '+00:00',
logging: false,
retryThrottle: 5000,
//forceSeed: true, --- ONLY IN DEVELOPMENT ---
//drop: true, --- ONLY IN DEVELOPMENT ---
},
bookkeeping: {
url: 'http://localhost:4000', // local insance
Expand Down
3 changes: 3 additions & 0 deletions QualityControl/docker-compose.coverage-ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
services:
test_app:
depends_on:
database:
condition: service_healthy
build:
context: .
target: coverage-ci
Expand Down
3 changes: 3 additions & 0 deletions QualityControl/docker-compose.coverage-local.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
services:
test_app:
depends_on:
database:
condition: service_healthy
build:
context: .
target: coverage-local
Expand Down
3 changes: 3 additions & 0 deletions QualityControl/docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
services:
application:
depends_on:
database:
condition: service_healthy
build:
context: .
target: development
Expand Down
3 changes: 3 additions & 0 deletions QualityControl/docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
services:
test_app:
depends_on:
database:
condition: service_healthy
build:
context: .
target: test
Expand Down
6 changes: 6 additions & 0 deletions QualityControl/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ services:
read_only: true
source: ./docker/database/populate
target: /docker-entrypoint-initdb.d
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 1m30s
timeout: 30s
retries: 5
start_period: 30s

volumes:
database-data:
3 changes: 2 additions & 1 deletion QualityControl/docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,5 @@ The application requires the following database configuration parameters:
| `timezone` | Time zone used for all date/time values in the database connection. |
| `logging` | Enables or disables SQL query logging (useful for debugging). |
| `retryThrottle` | Time in milliseconds to wait before retrying a failed database connection. |
| `migrationSeed` | *(Optional)* Set to `true` to execute seeders that populate the database with mock data. |
| `drop` | *(only in dev)* Set to `true` to drop all tables before the database migration. |
| `forceSeed` | *(only in dev)* Set to `true` to execute seeders that populate the database with mock data. |
4 changes: 3 additions & 1 deletion QualityControl/jsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@
"public/**/*.js",
"lib/**/*.js",
"test/**/*.js",
"lib/database/migrations/*.mjs" ]
"lib/database/migrations/*.mjs",
"lib/database/seeders/*.mjs"
]
}
81 changes: 58 additions & 23 deletions QualityControl/lib/QCModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,49 @@
* or submit itself to any jurisdiction.
*/

// Core dependencies
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { readFileSync } from 'fs';

// External dependencies
import { LogManager } from '@aliceo2/web-ui';
import { openFile, toJSON } from 'jsroot';
import { Kafka, logLevel } from 'kafkajs';

// Services
import { CcdbService } from './services/ccdb/CcdbService.js';
import { IntervalsService } from './services/Intervals.service.js';
import { StatusService } from './services/Status.service.js';
import { JsonFileService } from './services/JsonFileService.js';
import { QcObjectService } from './services/QcObject.service.js';
import { FilterService } from './services/FilterService.js';
import { BookkeepingService } from './services/BookkeepingService.js';
import { LayoutService } from './services/layout/LayoutService.js';
import { UserService } from './services/layout/UserService.js';
import { RunModeService } from './services/RunModeService.js';
import { AliEcsSynchronizer } from './services/external/AliEcsSynchronizer.js';
import { BookkeepingService } from './services/external/BookkeepingService.js';

//Controllers
import { LayoutController } from './controllers/LayoutController.js';
import { StatusController } from './controllers/StatusController.js';
import { ObjectController } from './controllers/ObjectController.js';
import { FilterController } from './controllers/FilterController.js';
import { UserController } from './controllers/UserController.js';

//Database
import { config } from './config/configProvider.js';
import { LayoutRepository } from './repositories/LayoutRepository.js';
import { UserRepository } from './repositories/UserRepository.js';
import { ChartRepository } from './repositories/ChartRepository.js';
import { initDatabase } from './database/index.js';
import { SequelizeDatabase } from './database/SequelizeDatabase.js';
import { setupRepositories } from './database/repositories/index.js';

//Middleware factories
import { objectGetByIdValidationMiddlewareFactory }
from './middleware/objects/objectGetByIdValidationMiddlewareFactory.js';
import { objectsGetValidationMiddlewareFactory } from './middleware/objects/objectsGetValidationMiddlewareFactory.js';
import { objectGetContentsValidationMiddlewareFactory }
from './middleware/objects/objectGetContentsValidationMiddlewareFactory.js';
import { RunModeService } from './services/RunModeService.js';

//DTOs
import { KafkaConfigDto } from './dtos/KafkaConfigurationDto.js';

const LOG_FACILITY = `${process.env.npm_config_log_label ?? 'qcg'}/model-setup`;
Expand All @@ -59,15 +67,12 @@ const LOG_FACILITY = `${process.env.npm_config_log_label ?? 'qcg'}/model-setup`;
export const setupQcModel = async (eventEmitter) => {
const logger = LogManager.getLogger(LOG_FACILITY);

// Load package metadata
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJSON = JSON.parse(readFileSync(`${__dirname}/../package.json`));

const jsonFileService = new JsonFileService(config.dbFile || `${__dirname}/../db.json`);
if (config.database) {
initDatabase(new SequelizeDatabase(config?.database || {}));
}

// Kafka setup (optional)
if (config?.kafka?.enabled) {
try {
const validConfig = await KafkaConfigDto.validateAsync(config.kafka);
Expand All @@ -85,47 +90,77 @@ export const setupQcModel = async (eventEmitter) => {
}
}

const layoutRepository = new LayoutRepository(jsonFileService);
const userRepository = new UserRepository(jsonFileService);
const chartRepository = new ChartRepository(jsonFileService);
// Database setup
const databaseConfig = config.database || {};
if (!databaseConfig || Object.keys(databaseConfig).length === 0) {
logger.errorMessage('Database configuration is not provided. The application cannot be initialized');
return;
}

const userController = new UserController(userRepository);
const layoutController = new LayoutController(layoutRepository);
const sequelizeDatabase = new SequelizeDatabase(databaseConfig);
initDatabase(sequelizeDatabase, { forceSeed: config?.database?.forceSeed, drop: config?.database?.drop });

const repositories = setupRepositories(sequelizeDatabase);
const {
userRepository,
layoutRepository,
tabRepository,
gridTabCellRepository,
chartRepository,
chartOptionRepository,
optionRepository,
} = repositories;

// Services
const layoutService = new LayoutService(
layoutRepository,
userRepository,
tabRepository,
gridTabCellRepository,
chartRepository,
chartOptionRepository,
optionRepository,
);
const userService = new UserService(userRepository);
const statusService = new StatusService({ version: packageJSON?.version ?? '-' }, { qc: config.qc ?? {} });
const statusController = new StatusController(statusService);

const ccdbService = CcdbService.setup(config.ccdb);
statusService.dataService = ccdbService;

const qcObjectService = new QcObjectService(ccdbService, chartRepository, { openFile, toJSON });
const qcObjectService = new QcObjectService(ccdbService, layoutService, { openFile, toJSON });
qcObjectService.refreshCache();

const intervalsService = new IntervalsService();

const bookkeepingService = new BookkeepingService(config.bookkeeping);
const filterService = new FilterService(bookkeepingService, config);
const runModeService = new RunModeService(config.bookkeeping, bookkeepingService, ccdbService, eventEmitter);
const objectController = new ObjectController(qcObjectService, runModeService);

// Controllers
const userController = new UserController(userService);
const layoutController = new LayoutController(layoutService);
const statusController = new StatusController(statusService);
const objectController = new ObjectController(qcObjectService, runModeService);
const filterController = new FilterController(filterService, runModeService);

// Middleware
const objectGetByIdValidation = objectGetByIdValidationMiddlewareFactory(filterService);
const objectsGetValidation = objectsGetValidationMiddlewareFactory(filterService);
const objectGetContentsValidation = objectGetContentsValidationMiddlewareFactory(filterService);

// Interval tasks
initializeIntervals(intervalsService, qcObjectService, filterService, runModeService);

// Return API
return {
userController,
layoutController,
userController,
statusService,
statusController,
objectController,
intervalsService,
filterController,
layoutRepository,
jsonFileService,
layoutService,
userService,
objectGetByIdValidation,
objectsGetValidation,
objectGetContentsValidation,
Expand Down
53 changes: 34 additions & 19 deletions QualityControl/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@
import { setupQcModel } from './QCModel.js';
import { minimumRoleMiddleware } from './middleware/minimumRole.middleware.js';
import { UserRole } from './../common/library/userRole.enum.js';

// Middlewares
import { layoutOwnerMiddleware } from './middleware/layouts/layoutOwner.middleware.js';
import { layoutIdMiddleware } from './middleware/layouts/layoutId.middleware.js';
import { layoutServiceMiddleware } from './middleware/layouts/layoutService.middleware.js';
import { statusComponentMiddleware } from './middleware/status/statusComponent.middleware.js';
import { runStatusFilterMiddleware } from './middleware/filters/runStatusFilter.middleware.js';
import { runModeMiddleware } from './middleware/filters/runMode.middleware.js';
import { getLayoutsMiddleware } from './middleware/layouts/layoutsGet.middleware.js';
import {
validateCreateLayoutMiddleware,
validatePatchLayoutMiddleware,
validateUpdateLayoutMiddleware,
} from './middleware/layouts/layoutValidate.middleware.js';
import { validateUserSession } from './middleware/validateUser.middleware.js';

/**
* Adds paths and binds websocket to instance of HttpServer passed
Expand All @@ -41,55 +49,60 @@ export const setup = async (http, ws, eventEmitter) => {
*/
const {
layoutController,
objectController,
statusController,
statusService,
userController,
layoutRepository,
jsonFileService,
statusService,
statusController,
objectController,
filterController,
layoutService,
userService,
objectGetByIdValidation,
objectsGetValidation,
objectGetContentsValidation,
} = await setupQcModel(eventEmitter);
statusService.ws = ws;

/* -------------------Objects ------------------------- */
http.get('/object/:id', objectGetByIdValidation, objectController.getObjectById.bind(objectController));
http.get('/object', objectGetContentsValidation, objectController.getObjectContent.bind(objectController));

http.get(
'/objects',
objectsGetValidation,
runModeMiddleware,
objectController.getObjects.bind(objectController),
);

http.get('/layouts', layoutController.getLayoutsHandler.bind(layoutController));
http.get('/layout/:id', layoutController.getLayoutHandler.bind(layoutController));
/* ------------------- Layouts ------------------------ */
http.get('/layouts', getLayoutsMiddleware, layoutController.getLayoutsHandler.bind(layoutController));
http.get('/layout/:id', layoutIdMiddleware(layoutService), layoutController.getLayoutHandler.bind(layoutController));
http.get('/layout', layoutController.getLayoutByNameHandler.bind(layoutController));
http.post('/layout', layoutController.postLayoutHandler.bind(layoutController));
http.post(
'/layout',
validateCreateLayoutMiddleware,
layoutController.postLayoutHandler.bind(layoutController),
);
http.put(
'/layout/:id',
layoutServiceMiddleware(jsonFileService),
layoutIdMiddleware(layoutRepository),
layoutOwnerMiddleware(layoutRepository),
layoutIdMiddleware(layoutService),
layoutOwnerMiddleware(layoutService, userService),
validateUpdateLayoutMiddleware,
layoutController.putLayoutHandler.bind(layoutController),
);
http.patch(
'/layout/:id',
layoutServiceMiddleware(jsonFileService),
layoutIdMiddleware(layoutRepository),
validatePatchLayoutMiddleware,
layoutIdMiddleware(layoutService),
minimumRoleMiddleware(UserRole.GLOBAL),
layoutController.patchLayoutHandler.bind(layoutController),
);
http.delete(
'/layout/:id',
layoutServiceMiddleware(jsonFileService),
layoutIdMiddleware(layoutRepository),
layoutOwnerMiddleware(layoutRepository),
layoutIdMiddleware(layoutService),
layoutOwnerMiddleware(layoutService, userService),
layoutController.deleteLayoutHandler.bind(layoutController),
);

/* ------------------- Status ------------------------- */
http.get('/status/gui', statusController.getQCGStatus.bind(statusController), { public: true });
http.get(
'/status/:service',
Expand All @@ -98,8 +111,10 @@ export const setup = async (http, ws, eventEmitter) => {
{ public: true },
);

http.get('/checkUser', userController.addUserHandler.bind(userController));
/* ------------------- Users -------------------------- */
http.get('/checkUser', validateUserSession, userController.addUserHandler.bind(userController));

/* ------------------- Filters ------------------------ */
http.get('/filter/configuration', filterController.getFilterConfigurationHandler.bind(filterController));
http.get(
'/filter/run-status/:runNumber',
Expand Down
2 changes: 2 additions & 0 deletions QualityControl/lib/config/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ export function getDbConfig(config) {
timezone: config.timezone ?? '+00:00',
logging: config.logging ?? false,
retryThrottle: config.retryThrottle ?? 5000,
forceSeed: config.forceSeed ?? false,
drop: config.drop ?? false,
};
};
Loading
Loading