Skip to content

feat: implement create-data link APIs - #112

Open
aboppay wants to merge 1 commit into
AudioReach:feature/use-case-designerfrom
aboppay:set-data-links
Open

aboppay wants to merge 1 commit into
AudioReach:feature/use-case-designerfrom
aboppay:set-data-links

Conversation

@aboppay

@aboppay aboppay commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

add core/persistence layer support to create data links.
Implement POST /data-links and /data-links/with-subsystems

@aboppay
aboppay requested review from a team, NithinSimon and houyu78 August 6, 2026 08:02
@aboppay
aboppay force-pushed the set-data-links branch 2 times, most recently from 3161b49 to 0b73d60 Compare August 12, 2026 05:41
Request body:

```
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it is always module, then why call it node? can't we use sourceModuleSystemId

@aboppay
aboppay force-pushed the set-data-links branch 2 times, most recently from 8784dc2 to 32d0f85 Compare August 13, 2026 16:15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files Not Fully Analyzed

  • docs/data-links/design/data-links-post-design.md - File extension .md not supported
  • docs/data-links/plans/data-links-post.md - File extension .md not supported

Qualcomm AI Review

Comment on lines +76 to +86
`DataLink for ports (${BinaryUtils.toHexString(srcPortId)}, ${BinaryUtils.toHexString(dstPortId)}) already exists.`,
);
}

// FR-DL-09: linkType derivation
// TODO: load srcModule.subgraphSystemId and dstModule.subgraphSystemId from overlay
const srcSubgraphId = 0;
const dstSubgraphId = 0;
const linkType = this.deriveLinkType(
command.isInterUsecase,
srcSubgraphId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functionality - High] Hardcoded subgraph IDs will cause incorrect linkType derivation

In CreateDataLinkHandler.handle(), the subgraph IDs are hardcoded to 0 instead of being loaded from the database. This will cause the deriveLinkType() method to always return LINK_TYPE.IntraSubgraph when isInterUsecase is not explicitly set to true, even when the modules are in different subgraphs.

The TODO comment on line 81 acknowledges this, but this is a critical functional issue that will cause incorrect link type classification and potentially break validation rules.

Impact: Links between modules in different subgraphs will be incorrectly classified as INTRA_SUBGRAPH instead of INTRA_USECASE, violating business rules.

Fixed Code Snippet
// FR-DL-09: linkType derivation
const moduleRepo = uow.getModuleRepository();
const srcModule = await moduleRepo.findBySystemId(srcModuleId, fileSystemId);
const dstModule = await moduleRepo.findBySystemId(dstModuleId, fileSystemId);

if (!srcModule || !dstModule) {
  throw new ResourceNotFoundException(
    `Module not found: ${!srcModule ? BinaryUtils.toHexString(srcModuleId) : BinaryUtils.toHexString(dstModuleId)}`
  );
}

const srcSubgraphId = srcModule.subgraphSystemId;
const dstSubgraphId = dstModule.subgraphSystemId;
const linkType = this.deriveLinkType(
  command.isInterUsecase,
  srcSubgraphId,
  dstSubgraphId,
);

Comment on lines 44 to +54
sessionId,
);
}

async createDataLink(
dataLink: DataLink,
boundaryPortPayloads: BoundaryPortPayload[],
options?: EditOptions,
): Promise<void> {
const {session, groupId} = this.uow.getWriteContext();
const fileSystemId = dataLink.fileSystemId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Error Handling - High] Missing error handling in createDataLink repository method

The TypeOrmDataLinkRepository.createDataLink() method performs multiple database writes in sequence but lacks proper error handling. If any of the write operations fail after some have succeeded, the method will throw an exception but won't roll back the partial changes, potentially leaving the database in an inconsistent state.

While the handler has transaction management, the repository method should handle its own errors gracefully and provide meaningful error messages.

Impact: Database inconsistency if writes fail partway through the operation.

Fixed Code Snippet
async createDataLink(
  dataLink: DataLink,
  boundaryPortPayloads: BoundaryPortPayload[],
  options?: EditOptions,
): Promise<void> {
  const {session, groupId} = this.uow.getWriteContext();
  const fileSystemId = dataLink.fileSystemId;
  const writer = this.requireWriter();

  try {
    // Write boundary nodes
    for (const bp of boundaryPortPayloads) {
      await writer.writeCreate(
        {
          targetTable: ENTITY_NAMES.Node,
          targetSystemId: bp.nodeSystemId,
          aggregateId: dataLink.systemId,
          payload: {
            type: 'subsystem',
            parentId: bp.nodeParentId ?? null,
            fileSystemId: bp.fileSystemId,
          },
          ...options,
        },
        session.sessionId,
        groupId,
        this.manager,
      );
    }

    // Write boundary ports
    for (const bp of boundaryPortPayloads) {
      await writer.writeCreate(
        {
          targetTable: ENTITY_NAMES.DataPort,
          targetSystemId: bp.portSystemId,
          aggregateId: dataLink.systemId,
          payload: {
            dataPortId: bp.dataPortId,
            portIoType: bp.portIoType,
            isStatic: false,
            name: '',
            nodeSystemId: bp.nodeSystemId,
            fileSystemId: bp.fileSystemId,
          },
          ...options,
        },
        session.sessionId,
        groupId,
        this.manager,
      );
    }

    // Write DataLink
    await writer.writeCreate(
      {
        targetTable: ENTITY_NAMES.DataLink,
        targetSystemId: dataLink.systemId,
        aggregateId: dataLink.systemId,
        payload: {
          sourceNodeSystemId: dataLink.sourceNodeSystemId,
          destinationNodeSystemId: dataLink.destinationNodeSystemId,
          sourcePortSystemId: dataLink.sourcePortSystemId,
          destinationPortSystemId: dataLink.destinationPortSystemId,
          linkType: dataLink.linkType,
          sourceSubgraphSystemId: dataLink.sourceSubgraphSystemId,
          destSubgraphSystemId: dataLink.destSubgraphSystemId,
          isEc: dataLink.isEc ?? null,
          fileSystemId,
        },
        ...options,
      },
      session.sessionId,
      groupId,
      this.manager,
    );

    // Write SubsystemDataLinks
    for (const sls of dataLink.subsystemDataLinks) {
      await writer.writeCreate(
        {
          targetTable: ENTITY_NAMES.SubsystemDataLink,
          targetSystemId: sls.systemId,
          aggregateId: dataLink.systemId,
          payload: {
            sourceNodeSystemId: sls.sourceNodeSystemId,
            destinationNodeSystemId: sls.destinationNodeSystemId,
            sourcePortSystemId: sls.sourcePortSystemId,
            destinationPortSystemId: sls.destinationPortSystemId,
            dataLinkSystemId: sls.dataLinkSystemId,
            fileSystemId,
          },
          ...options,
        },
        session.sessionId,
        groupId,
        this.manager,
      );
    }
  } catch (error) {
    throw new Error(
      `Failed to create DataLink ${dataLink.systemId}: ${error instanceof Error ? error.message : String(error)}`
    );
  }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files Not Fully Analyzed

  • docs/data-links/design/data-links-post-design.md - File extension .md not supported
  • docs/data-links/plans/2026-08-18-data-links-post-fix-plan.md - File extension .md not supported
  • docs/data-links/plans/data-links-post.md - File extension .md not supported
  • packages/api/src/infrastructure-wrapper/filters/all-exceptions.filter.ts - Full file content skipped due to token limit
  • packages/api/src/infrastructure-wrapper/persistence/unit-of-work/typeorm-unit-of-work.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/swagger-doc/dto-examples/usecase-api-examples.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/spf-module/spf-module.controller.ts - Full file content skipped due to token limit
  • packages/core/src/application/orchestration/cqrs/registries/command-handler-registry.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/subsystem-data-link-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/usecase-components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.command.ts - Full file content skipped due to token limit
  • packages/core/src/index.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/conflict.exception.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/index.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/file-operations/upload-file/services/entity-builders/subsystem-builder.spec.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/module/module.repository.ts - Full file content skipped due to token limit

Qualcomm AI Review

Comment on lines +278 to +288
fileSystemId: number,
uow: UnitOfWork,
subsystemRepo: ReturnType<UnitOfWork['getSubsystemRepository']>,
): Promise<ComponentCollectionWithSubsystemsDto> {
// Branch B (FR-DLS-11): at least one endpoint is a subsystem
if (command.isInterUsecase !== undefined || command.isEc !== undefined) {
throw new DomainRuleViolationException([{
code: 'INVALID_FLAGS_FOR_SUBSYSTEM',
message: 'isInterUsecase and isEc must not be provided when a subsystem endpoint is involved.',
severity: IssueSeverity.Error,
}]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functionality] Missing null check for module lookup in subsystem endpoint validation (Medium Severity)

In create-data-link-with-subsystems.handler.ts, when Branch B (subsystem endpoint) is triggered, the code doesn't validate whether the non-subsystem endpoint is actually a valid module. If a user provides a subsystem ID as source and an invalid/non-existent ID as destination (that's neither a module nor a subsystem), the validation will fail with a generic "port not found" error instead of a more specific "node not found" error.

The issue occurs because in Branch B, the code only checks subsystemExists() for both endpoints but doesn't verify that the non-subsystem endpoint is actually a valid module node. This could lead to confusing error messages.

Impact: Users may receive misleading error messages when providing invalid node IDs in subsystem link creation scenarios.

Fixed Code Snippet
private async handleBranchB(
  command: CreateDataLinkWithSubsystemsCommand,
  srcNodeId: number,
  dstNodeId: number,
  srcPortId: number,
  dstPortId: number,
  srcIsSubsystem: boolean,
  dstIsSubsystem: boolean,
  fileSystemId: number,
  uow: UnitOfWork,
  subsystemRepo: ReturnType<UnitOfWork['getSubsystemRepository']>,
): Promise<ComponentCollectionWithSubsystemsDto> {
  // Branch B (FR-DLS-11): at least one endpoint is a subsystem
  if (command.isInterUsecase !== undefined || command.isEc !== undefined) {
    throw new DomainRuleViolationException([{
      code: 'INVALID_FLAGS_FOR_SUBSYSTEM',
      message: 'isInterUsecase and isEc must not be provided when a subsystem endpoint is involved.',
      severity: IssueSeverity.Error,
    }]);
  }

  // Validate that non-subsystem endpoints are valid modules
  const moduleRepo = uow.getModuleRepository();
  if (!srcIsSubsystem) {
    const srcModule = await moduleRepo.findModulePortsForLink(srcNodeId, fileSystemId);
    if (srcModule === null) {
      throw new ResourceNotFoundException(
        `Source node ${BinaryUtils.toHexString(srcNodeId)} not found or is not a valid module.`,
      );
    }
  }
  if (!dstIsSubsystem) {
    const dstModule = await moduleRepo.findModulePortsForLink(dstNodeId, fileSystemId);
    if (dstModule === null) {
      throw new ResourceNotFoundException(
        `Destination node ${BinaryUtils.toHexString(dstNodeId)} not found or is not a valid module.`,
      );
    }
  }

  // FR-DLS-03 + FR-DLS-08 + FR-DLS-07: validate subsystem-side ports
  if (srcIsSubsystem) {
    const srcPortType = await subsystemRepo.getPortIoType(srcPortId, fileSystemId);
    // ... rest of validation
  }
  // ... rest of the method
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files Not Fully Analyzed

  • docs/data-links/design/data-links-post-design.md - File extension .md not supported
  • packages/api/src/infrastructure-wrapper/filters/all-exceptions.filter.ts - Full file content skipped due to token limit
  • packages/api/src/infrastructure-wrapper/persistence/unit-of-work/typeorm-unit-of-work.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/swagger-doc/dto-examples/usecase-api-examples.ts - Full file content skipped due to token limit
  • packages/core/src/application/orchestration/cqrs/registries/command-handler-registry.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/subsystem-data-link-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/usecase-components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.command.ts - Full file content skipped due to token limit
  • packages/core/src/index.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/index.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/file-operations/upload-file/services/entity-builders/subsystem-builder.spec.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/module/module.repository.ts - Full file content skipped due to token limit

Qualcomm AI Review

Comment on lines +210 to +220
aggregateId: number,
payload: Record<string, unknown>,
options?: EditOptions,
): Promise<void> {
const {session, groupId} = this.uow.getWriteContext();
const writer = this.requireWriter();
// eslint-disable-next-line custom/no-raw-persistence-queries -- conditional UPDATE with IS NULL on valid_until cannot be expressed with TypeORM QueryBuilder
await this.manager.query(
`UPDATE edit_actions SET valid_until = $1 WHERE session_id = $2 AND target_system_id = $3 AND field_path IS NULL AND valid_until IS NULL`,
[new Date().toISOString(), session.sessionId, systemId],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SECURITY][High] SQL Injection Vulnerability in reactivateDataLink

The reactivateDataLink method in TypeOrmDataLinkRepository uses raw SQL with string interpolation for the valid_until timestamp, which could be exploited if the date format is manipulated. While the current implementation uses new Date().toISOString(), this pattern is risky and violates the principle of using parameterized queries consistently.

Issue Location: packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/data-link/data-link.repository.ts:217-220

Risk: If the date generation logic is ever modified or if there's any way to influence the timestamp value, this could lead to SQL injection.

Fixed Code Snippet
async reactivateDataLink(
  systemId: number,
  aggregateId: number,
  payload: Record<string, unknown>,
  options?: EditOptions,
): Promise<void> {
  const {session, groupId} = this.uow.getWriteContext();
  const writer = this.requireWriter();
  const validUntil = new Date().toISOString();
  // Use parameterized query for all values
  await this.manager.query(
    `UPDATE edit_actions SET valid_until = $1 WHERE session_id = $2 AND target_system_id = $3 AND field_path IS NULL AND valid_until IS NULL`,
    [validUntil, session.sessionId, systemId],
  );
  await writer.writeCreate(
    {
      targetTable: ENTITY_NAMES.DataLink,
      targetSystemId: systemId,
      aggregateId,
      payload,
      ...options,
    },
    session.sessionId,
    groupId,
    this.manager,
  );
}

The fix extracts the timestamp to a variable first, making the code more maintainable and ensuring the parameterized query pattern is clear.

Comment on lines +266 to +276
// FR-DLS-14: DataLink persisted internally but excluded from response
return emptyCollection(slsSegments.map(sls => mapSubsystemDataLink(sls)));
}

private async handleBranchB(
command: CreateDataLinkWithSubsystemsCommand,
srcNodeId: number,
dstNodeId: number,
srcPortId: number,
dstPortId: number,
srcIsSubsystem: boolean,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY][High] Missing Transaction Rollback in Branch B Error Path

In CreateDataLinkWithSubsystemsHandler.handleBranchB, if an exception occurs after validation but before commit, the transaction is not rolled back because the catch block in the main handle method only checks uow.isInTransaction(). However, if the transaction was started but an error occurs in handleBranchB, the rollback won't happen since handleBranchB doesn't have its own try-catch.

Issue Location: packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.ts:270-355

Risk: Database inconsistency if an error occurs during Branch B processing (subsystem endpoint creation). The transaction would remain open and uncommitted.

Fixed Code Snippet
private async handleBranchB(
  command: CreateDataLinkWithSubsystemsCommand,
  srcNodeId: number,
  dstNodeId: number,
  srcPortId: number,
  dstPortId: number,
  srcIsSubsystem: boolean,
  dstIsSubsystem: boolean,
  fileSystemId: number,
  uow: UnitOfWork,
  subsystemRepo: ReturnType<UnitOfWork['getSubsystemRepository']>,
): Promise<ComponentCollectionWithSubsystemsDto> {
  try {
    // Branch B (FR-DLS-11): at least one endpoint is a subsystem
    if (command.isInterUsecase !== undefined || command.isEc !== undefined) {
      throw new DomainRuleViolationException([{
        code: 'INVALID_FLAGS_FOR_SUBSYSTEM',
        message: 'isInterUsecase and isEc must not be provided when a subsystem endpoint is involved.',
        severity: IssueSeverity.Error,
      }]);
    }

    // ... rest of validation and creation logic ...

    const dlEditRepo = uow.getDataLinkRepository();
    await dlEditRepo.createSubsystemDataLink(sls);
    await uow.commit();
    return emptyCollection([mapSubsystemDataLink(sls)]);
  } catch (error) {
    if (uow.isInTransaction()) {
      await uow.rollback();
    }
    throw error;
  }
}

Alternatively, the main handle method's catch block should be sufficient if it properly catches all errors from handleBranchB. Verify that the transaction state is correctly managed.

Comment on lines +155 to +197
}])
: new ResourceNotFoundException(`Destination module ${BinaryUtils.toHexString(dstNodeId)} not found.`);
}

// FR-DLS-06: port ownership
const srcPort = srcModule.ports.find(p => p.systemId === srcPortId);
if (!srcPort) {
throw new DomainRuleViolationException([{
code: 'PORT_OWNERSHIP_MISMATCH',
message: `Port ${BinaryUtils.toHexString(srcPortId)} does not belong to source module — ownership check failed.`,
severity: IssueSeverity.Error,
}]);
}
const dstPort = dstModule.ports.find(p => p.systemId === dstPortId);
if (!dstPort) {
throw new DomainRuleViolationException([{
code: 'PORT_OWNERSHIP_MISMATCH',
message: `Port ${BinaryUtils.toHexString(dstPortId)} does not belong to destination module — ownership check failed.`,
severity: IssueSeverity.Error,
}]);
}

// FR-DLS-05: port direction
if (srcPort.portIoType !== PORT_IO_TYPE.Output) {
throw new DomainRuleViolationException([{
code: 'WRONG_PORT_DIRECTION',
message: `Source port must be OUTPUT, got ${srcPort.portIoType}.`,
severity: IssueSeverity.Error,
}]);
}
if (dstPort.portIoType !== PORT_IO_TYPE.Input) {
throw new DomainRuleViolationException([{
code: 'WRONG_PORT_DIRECTION',
message: `Destination port must be INPUT, got ${dstPort.portIoType}.`,
severity: IssueSeverity.Error,
}]);
}

const dlEditRepo = uow.getDataLinkRepository();

const existing = await dlEditRepo.findByPortPair(srcPortId, dstPortId, fileSystemId);
if (existing !== null && !existing.isDeleted) {
throw new ConflictException(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAINTAINABILITY][Medium] Inconsistent Error Handling Between Branch A and Branch B

In CreateDataLinkWithSubsystemsHandler, Branch A (module-to-module) has comprehensive error handling with specific exception types for different validation failures, while Branch B (subsystem endpoint) uses a simpler validation approach. This inconsistency makes the code harder to maintain and could lead to different error experiences for users.

Issue Location: packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.ts:121-268 vs 270-355

Recommendation: Standardize the error handling pattern across both branches. Consider extracting common validation logic into separate methods to reduce duplication and ensure consistent error messages.

Example Refactoring:

private validatePortOwnership(
  port: {systemId: number; portIoType: PortIoType} | undefined,
  portId: number,
  nodeType: 'source' | 'destination',
): void {
  if (!port) {
    throw new DomainRuleViolationException([{
      code: 'PORT_OWNERSHIP_MISMATCH',
      message: `Port ${BinaryUtils.toHexString(portId)} does not belong to ${nodeType} module — ownership check failed.`,
      severity: IssueSeverity.Error,
    }]);
  }
}

private validatePortDirection(
  port: {systemId: number; portIoType: PortIoType},
  expectedType: PortIoType,
  portId: number,
  nodeType: 'source' | 'destination',
): void {
  if (port.portIoType !== expectedType) {
    throw new DomainRuleViolationException([{
      code: 'WRONG_PORT_DIRECTION',
      message: `${nodeType === 'source' ? 'Source' : 'Destination'} port must be ${expectedType}, got ${port.portIoType}.`,
      severity: IssueSeverity.Error,
    }]);
  }
}

Then use these methods consistently in both branches.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Qualcomm AI Review

Comment on lines 271 to 280
sourceNodeSystemId: Number(row.sourceNodeSystemId),
destinationNodeSystemId: Number(row.destinationNodeSystemId),
sourcePortSystemId: Number(row.sourcePortSystemId),
destinationPortSystemId: Number(row.destinationPortSystemId),
dataLinkSystemId:
row.dataLinkSystemId != null ? Number(row.dataLinkSystemId) : null,
}),
),
);
} catch (error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functionality] Potential null handling inconsistency in SubsystemDataLinkReadModel mapping - Medium Severity

In db-subsystem-query-service.ts, the mapper for SubsystemDataLinkReadModel checks if row.dataLinkSystemId != null before converting to number, but the type definition allows null. However, the SQL query joins with data_links using INNER JOIN, which means dl.system_id should never be null in practice. This creates a potential inconsistency:

  1. The INNER JOIN guarantees non-null values
  2. The mapper defensively handles null
  3. The type allows null

This could lead to confusion about when null values are actually expected. Consider either:

  • Using LEFT JOIN if null values are legitimately expected (e.g., for unresolved segments)
  • Removing the null check and updating the type to number if the join guarantees non-null
  • Adding a comment explaining why the defensive null check exists despite the INNER JOIN

Current Implementation:

dataLinkSystemId:
  row.dataLinkSystemId != null ? Number(row.dataLinkSystemId) : null,

Recommended Fix:
If null values should never occur with the current INNER JOIN, update the type and remove the defensive check:

// In SubsystemDataLinkReadModel type definition
dataLinkSystemId: number;  // Remove | null if INNER JOIN guarantees non-null

// In mapper
dataLinkSystemId: Number(row.dataLinkSystemId),

Alternatively, if null values are expected for future use cases (e.g., unresolved segments), change to LEFT JOIN and add a comment:

// Use LEFT JOIN if segments can exist without parent links
.leftJoin('data_links', 'dl', 'dl.system_id = sdl.data_link_system_id')

// In mapper with comment
dataLinkSystemId:
  row.dataLinkSystemId != null ? Number(row.dataLinkSystemId) : null,  // null for unresolved segments

@aboppay
aboppay force-pushed the set-data-links branch 2 times, most recently from 0185409 to 6e06894 Compare August 21, 2026 11:15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Qualcomm AI Review

Comment on lines +213 to +222
const dstSubgraphId = dstModule.subgraphSystemId;
const linkType = deriveLinkType(command.isInterUsecase, srcSubgraphId, dstSubgraphId);

if (linkType === LINK_TYPE.InterUsecase) {
const subgraphRepo = uow.getSubgraphRepository();
const [srcUsecaseId, dstUsecaseId] = await Promise.all([
subgraphRepo.getUsecaseSystemIdForSubgraph(srcSubgraphId, fileSystemId),
subgraphRepo.getUsecaseSystemIdForSubgraph(dstSubgraphId, fileSystemId),
]);
if (srcUsecaseId !== null && dstUsecaseId !== null && srcUsecaseId === dstUsecaseId) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY] Missing null safety check in inter-usecase validation - Medium Severity

In create-data-link-with-subsystems.handler.ts at line 222, the code checks if both srcUsecaseId and dstUsecaseId are non-null before comparing them. However, if either value is null, the validation silently passes, which could allow invalid inter-usecase links to be created when subgraph-to-usecase mapping fails.

The same issue exists in create-data-link.handler.ts at line 175.

Issue: If getUsecaseSystemIdForSubgraph() returns null for either subgraph (e.g., orphaned subgraph not associated with any usecase), the validation is skipped entirely. This could lead to data integrity issues where isInterUsecase=true is set but the link doesn't actually cross usecase boundaries.

Recommendation: Add explicit handling for the null case to either throw an error or log a warning when usecase mapping fails.

Fixed Code Snippet
if (linkType === LINK_TYPE.InterUsecase) {
  const subgraphRepo = uow.getSubgraphRepository();
  const [srcUsecaseId, dstUsecaseId] = await Promise.all([
    subgraphRepo.getUsecaseSystemIdForSubgraph(srcSubgraphId, fileSystemId),
    subgraphRepo.getUsecaseSystemIdForSubgraph(dstSubgraphId, fileSystemId),
  ]);
  
  // Validate that both subgraphs belong to usecases
  if (srcUsecaseId === null || dstUsecaseId === null) {
    throw new DomainRuleViolationException([{
      code: 'ORPHANED_SUBGRAPH',
      message: 'Cannot create inter-usecase link: one or both subgraphs are not associated with a usecase.',
      severity: IssueSeverity.Error,
    }]);
  }
  
  if (srcUsecaseId === dstUsecaseId) {
    throw new DomainRuleViolationException([{
      code: 'SAME_USECASE_INTER_USECASE',
      message: 'isInterUsecase=true but source and destination belong to the same usecase.',
      severity: IssueSeverity.Error,
    }]);
  }
}

Comment on lines +165 to +175
command.isInterUsecase,
srcSubgraphId,
dstSubgraphId,
);
if (linkType === LINK_TYPE.InterUsecase) {
const subgraphRepo = uow.getSubgraphRepository();
const [srcUsecaseId, dstUsecaseId] = await Promise.all([
subgraphRepo.getUsecaseSystemIdForSubgraph(srcSubgraphId, fileSystemId),
subgraphRepo.getUsecaseSystemIdForSubgraph(dstSubgraphId, fileSystemId),
]);
if (srcUsecaseId !== null && dstUsecaseId !== null && srcUsecaseId === dstUsecaseId) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY] Missing null safety check in inter-usecase validation (duplicate location) - Medium Severity

In create-data-link.handler.ts at line 169, the same null safety issue exists as in the previous finding. The code checks if both srcUsecaseId and dstUsecaseId are non-null before comparing them, but silently allows the operation to proceed if either is null.

Issue: This is the same logical flaw as in create-data-link-with-subsystems.handler.ts. When getUsecaseSystemIdForSubgraph() returns null, the validation is bypassed, potentially allowing invalid inter-usecase links.

Recommendation: Apply the same fix as the previous issue to ensure consistent validation across both handlers.

Fixed Code Snippet
if (linkType === LINK_TYPE.InterUsecase) {
  const subgraphRepo = uow.getSubgraphRepository();
  const [srcUsecaseId, dstUsecaseId] = await Promise.all([
    subgraphRepo.getUsecaseSystemIdForSubgraph(srcSubgraphId, fileSystemId),
    subgraphRepo.getUsecaseSystemIdForSubgraph(dstSubgraphId, fileSystemId),
  ]);
  
  if (srcUsecaseId === null || dstUsecaseId === null) {
    throw new DomainRuleViolationException([{
      code: 'ORPHANED_SUBGRAPH',
      message: 'Cannot create inter-usecase link: one or both subgraphs are not associated with a usecase.',
      severity: IssueSeverity.Error,
    }]);
  }
  
  if (srcUsecaseId === dstUsecaseId) {
    throw new DomainRuleViolationException([{
      code: 'SAME_USECASE_INTER_USECASE',
      message: 'isInterUsecase=true but source and destination belong to the same usecase.',
      severity: IssueSeverity.Error,
    }]);
  }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files Not Fully Analyzed

  • docs/data-links/design/data-links-post-design.md - File extension .md not supported
  • packages/api/src/infrastructure-wrapper/filters/all-exceptions.filter.ts - Full file content skipped due to token limit
  • packages/api/src/infrastructure-wrapper/persistence/unit-of-work/typeorm-unit-of-work.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/swagger-doc/dto-examples/usecase-api-examples.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/data-link.controller.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-request.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-with-subsystems-request.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/response/subsystem-data-link-response.dto.ts - Full file content skipped due to token limit
  • packages/core/src/application/file-operations/upload-file/services/entity-builders/subsystem-builder.ts - Full file content skipped due to token limit
  • packages/core/src/application/orchestration/cqrs/registries/command-handler-registry.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/subsystem/subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/subsystem-data-link-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/usecase-components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/build-traversal-entities.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.handler.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/dto/component-collection-dto.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/build-subsystem-tree.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.ts - Full file content skipped due to token limit
  • packages/core/src/domain/services/subsystem-data-links/subsystem-data-link-derivation.service.ts - Full file content skipped due to token limit
  • packages/core/src/index.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/index.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/file-operations/upload-file/services/entity-builders/subsystem-builder.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/domain/services/subsystem-links/subsystem-data-link-derivation.service.spec.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/subsystem/db-subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/tests/integration/repositories/data-link/data-link-edit.repository.integration.spec.ts - Full file content skipped due to token limit

Qualcomm AI Review

Comment on lines +213 to +223
): Promise<void> {
const {session, groupId} = this.uow.getWriteContext();
const writer = this.requireWriter();
// eslint-disable-next-line custom/no-raw-persistence-queries -- conditional UPDATE with IS NULL on valid_until cannot be expressed with TypeORM QueryBuilder
await this.manager.query(
`UPDATE edit_actions SET valid_until = $1 WHERE session_id = $2 AND target_system_id = $3 AND field_path IS NULL AND valid_until IS NULL`,
[new Date().toISOString(), session.sessionId, systemId],
);
await writer.writeCreate(
{
targetTable: ENTITY_NAMES.DataLink,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SECURITY][High] SQL Injection Vulnerability in reactivateDataLink

The reactivateDataLink method in TypeOrmDataLinkRepository uses raw SQL with string interpolation for the valid_until update. While the current implementation uses parameterized queries for session_id and target_system_id, the date value is directly interpolated using toISOString(). This could potentially be exploited if the date object is manipulated.

Issue Location: packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/data-link/data-link.repository.ts:218

Risk: An attacker who can control the system clock or manipulate Date objects could potentially inject SQL through the timestamp value.

Recommendation: Use parameterized queries consistently for all values, including timestamps.

Fixed Code Snippet
await this.manager.query(
  `UPDATE edit_actions SET valid_until = $1 WHERE session_id = $2 AND target_system_id = $3 AND field_path IS NULL AND valid_until IS NULL`,
  [new Date(), session.sessionId, systemId],
);

Note: Pass the Date object directly instead of calling toISOString() - the database driver will handle the conversion safely.

Comment on lines +287 to +297
fileSystemId,
isEc,
subsystemDataLinks: slsSegments,
});

await dlEditRepo.createDataLink(dataLink, boundaryPortPayloads);
await uow.commit();

return this.buildDto(
dataLink.systemId,
dataLink.sourceNodeSystemId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY][High] Race Condition in Duplicate Detection Logic

The findByPortPair method in both handler implementations checks for existing data links, but there's a race condition between the check and the actual creation. Two concurrent requests with the same port pair could both pass the duplicate check and create duplicate links.

Issue Location: packages/core/src/application/usecase-designer/data-links/create/create-data-link.handler.ts:151-161

Scenario:

  1. Request A calls findByPortPair → returns null
  2. Request B calls findByPortPair → returns null (before A commits)
  3. Request A creates the link and commits
  4. Request B creates a duplicate link and commits

Impact: Database integrity violation, duplicate data links in the system.

Recommendation: Add a unique constraint on (source_port_system_id, destination_port_system_id, file_system_id) in the data_links table and handle the constraint violation exception. Alternatively, use database-level locking (SELECT FOR UPDATE) during the check.

Fixed Code Snippet
const existing = await dlEditRepo.findByPortPair(
  srcPortId,
  dstPortId,
  fileSystemId,
);

if (existing !== null && !existing.isDeleted) {
  throw new ConflictException(
    `DataLink for ports (${BinaryUtils.toHexString(srcPortId)}, ${BinaryUtils.toHexString(dstPortId)}) already exists.`,
  );
}

// Add database-level unique constraint:
// ALTER TABLE data_links ADD CONSTRAINT uq_data_link_ports 
// UNIQUE (source_port_system_id, destination_port_system_id, file_system_id);

try {
  await dlEditRepo.createDataLink(dataLink, boundaryPortPayloads);
  await uow.commit();
} catch (error) {
  if (error.code === 'SQLITE_CONSTRAINT' || error.code === '23505') {
    throw new ConflictException(
      `DataLink for ports (${BinaryUtils.toHexString(srcPortId)}, ${BinaryUtils.toHexString(dstPortId)}) already exists.`,
    );
  }
  throw error;
}

Comment on lines +111 to +121
fileSystemId,
uow,
subsystemRepo,
);
} catch (error) {
if (uow.isInTransaction()) await uow.rollback();
throw error;
}
}

private async handleBranchA(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY][High] Missing Transaction Rollback in Error Handler

In CreateDataLinkWithSubsystemsHandler.handleBranchB, if an exception occurs after validation but before commit, the transaction is not rolled back. The catch block in the main handle method only rolls back if uow.isInTransaction() returns true, but this check may not be reliable if the transaction state is corrupted.

Issue Location: packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.ts:115-118

Problem: If createSubsystemDataLink throws an exception, the transaction remains open and uncommitted, potentially causing connection leaks or blocking other operations.

Recommendation: Ensure rollback is called in all error paths, and consider using try-finally blocks for cleanup.

Fixed Code Snippet
async handle(
  command: CreateDataLinkWithSubsystemsCommand,
): Promise<ComponentCollectionWithSubsystemsDto> {
  const uow = this.uow;
  await uow.startTransaction();
  try {
    const {session} = uow.getWriteContext();
    const fileSystemId = session.fileSystemId;
    // ... validation and processing logic ...
    
    if (srcIsSubsystem || dstIsSubsystem) {
      return await this.handleBranchB(
        command,
        srcNodeId,
        dstNodeId,
        srcPortId,
        dstPortId,
        srcIsSubsystem,
        dstIsSubsystem,
        fileSystemId,
        uow,
        subsystemRepo,
      );
    }
    
    return await this.handleBranchA(
      command,
      srcNodeId,
      dstNodeId,
      srcPortId,
      dstPortId,
      fileSystemId,
      uow,
      subsystemRepo,
    );
  } catch (error) {
    // Always attempt rollback, regardless of transaction state
    try {
      if (uow.isInTransaction()) {
        await uow.rollback();
      }
    } catch (rollbackError) {
      // Log rollback failure but throw original error
      console.error('Failed to rollback transaction:', rollbackError);
    }
    throw error;
  }
}

Comment on lines +53 to +63
}
return map;
}

async getPortIoType(
portSystemId: number,
fileSystemId: number,
): Promise<PortIoType | null> {
const sessionId = this.uow.getWriteContext().session.sessionId;

// Check session overlay first — a staged CREATE wins over the base table

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[PERFORMANCE][Medium] Inefficient Sequential Database Queries in Validation

The getPortIoType method in TypeOrmSubsystemRepository performs two separate database queries: first checking the session overlay, then falling back to the base table. For validation scenarios where multiple ports are checked (source and destination in Branch B), this results in 4+ sequential queries.

Issue Location: packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subsystem/subsystem.repository.ts:57-92

Impact: Increased latency for link creation operations, especially when subsystem endpoints are involved.

Recommendation: Batch the port lookups or use a single query with UNION to check both overlay and base table simultaneously.

Fixed Code Snippet
async getPortIoType(
  portSystemId: number,
  fileSystemId: number,
): Promise<PortIoType | null> {
  const sessionId = this.uow.getWriteContext().session.sessionId;

  // Single query combining overlay and base table
  const result = await this.manager.query(
    `
    SELECT port_io_type as portIoType FROM (
      SELECT 
        CAST(json_extract(new_value, '$.portIoType') AS TEXT) as port_io_type,
        1 as priority
      FROM edit_actions
      WHERE session_id = $1 
        AND target_table = 'DataPort'
        AND target_system_id = $2
        AND operation = 'CREATE'
        AND valid_until IS NULL
      UNION ALL
      SELECT 
        port_io_type,
        2 as priority
      FROM data_ports
      WHERE system_id = $2 AND file_system_id = $3
    ) combined
    ORDER BY priority
    LIMIT 1
    `,
    [sessionId, portSystemId, fileSystemId],
  );

  return result.length > 0 ? (result[0].portIoType as PortIoType) : null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files Not Fully Analyzed

  • docs/data-links/design/data-links-post-design.md - File extension .md not supported
  • packages/api/src/infrastructure-wrapper/filters/all-exceptions.filter.ts - Full file content skipped due to token limit
  • packages/api/src/infrastructure-wrapper/persistence/unit-of-work/typeorm-unit-of-work.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/swagger-doc/dto-examples/usecase-api-examples.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/data-link.controller.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-request.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-with-subsystems-request.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/response/subsystem-data-link-response.dto.ts - Full file content skipped due to token limit
  • packages/api/tests/e2e/data-links/create-data-link.e2e-spec.ts - Full file content skipped due to token limit
  • packages/core/src/application/file-operations/upload-file/services/entity-builders/subsystem-builder.ts - Full file content skipped due to token limit
  • packages/core/src/application/orchestration/cqrs/registries/command-handler-registry.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/subsystem/subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/subsystem-data-link-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/usecase-components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/build-traversal-entities.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.handler.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/dto/component-collection-dto.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/build-subsystem-tree.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.ts - Full file content skipped due to token limit
  • packages/core/src/domain/services/subsystem-data-links/subsystem-data-link-derivation.service.ts - Full file content skipped due to token limit
  • packages/core/src/index.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/conflict.exception.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/index.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/file-operations/upload-file/services/entity-builders/subsystem-builder.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/subsystem/db-subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/tests/integration/repositories/data-link/data-link-edit.repository.integration.spec.ts - Full file content skipped due to token limit

Qualcomm AI Review

Comment on lines 33 to +43
private readonly uow: UnitOfWork,
private readonly queryServices: QueryServices,
private readonly idGeneration: IdGenerationPort,
) {}

handle(_command: CreateDataLinkCommand): Promise<ComponentCollectionDto> {
if (
this.uow == undefined ||
this.queryServices == undefined ||
this.idGeneration == undefined
)
throw new Error('Input validation error');
throw new Error('Not implemented');
async handle(
command: CreateDataLinkCommand,
): Promise<ComponentCollectionDto> {
const uow = this.uow;
await uow.startTransaction();
try {
const {session} = uow.getWriteContext();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY][High] Missing rollback in catch block for transaction error handling

In create-data-link.handler.ts and create-data-link-with-subsystems.handler.ts, the catch blocks check uow.isInTransaction() before calling rollback(), but if the transaction was never started due to an early error, or if startTransaction() itself throws, the error handling becomes inconsistent. Additionally, if rollback() itself throws an exception, the original error will be masked.

Issue: The current pattern can lead to:

  1. Unhandled transaction state if startTransaction() fails
  2. Original error being masked if rollback() throws
  3. Resource leaks if transaction cleanup fails silently
Fixed Code Snippet
async handle(
  command: CreateDataLinkCommand,
): Promise<ComponentCollectionDto> {
  const uow = this.uow;
  let transactionStarted = false;
  try {
    await uow.startTransaction();
    transactionStarted = true;
    const {session} = uow.getWriteContext();
    // ... rest of handler logic
    await uow.commit();
    return this.buildDto(/* ... */);
  } catch (error) {
    if (transactionStarted) {
      try {
        await uow.rollback();
      } catch (rollbackError) {
        // Log rollback failure but don't mask original error
        console.error('Rollback failed:', rollbackError);
      }
    }
    throw error;
  }
}

Comment on lines +57 to +67
private readonly uow: UnitOfWork,
private readonly idGeneration: IdGenerationPort,
) {}

async handle(
command: CreateDataLinkWithSubsystemsCommand,
): Promise<ComponentCollectionWithSubsystemsDto> {
const uow = this.uow;
await uow.startTransaction();
try {
const {session} = uow.getWriteContext();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY][High] Missing rollback in catch block for transaction error handling

In create-data-link.handler.ts and create-data-link-with-subsystems.handler.ts, the catch blocks check uow.isInTransaction() before calling rollback(), but if the transaction was never started due to an early error, or if startTransaction() itself throws, the error handling becomes inconsistent. Additionally, if rollback() itself throws an exception, the original error will be masked.

Issue: The current pattern can lead to:

  1. Unhandled transaction state if startTransaction() fails
  2. Original error being masked if rollback() throws
  3. Resource leaks if transaction cleanup fails silently
Fixed Code Snippet
async handle(
  command: CreateDataLinkCommand,
): Promise<ComponentCollectionDto> {
  const uow = this.uow;
  let transactionStarted = false;
  try {
    await uow.startTransaction();
    transactionStarted = true;
    const {session} = uow.getWriteContext();
    // ... rest of handler logic
    await uow.commit();
    return this.buildDto(/* ... */);
  } catch (error) {
    if (transactionStarted) {
      try {
        await uow.rollback();
      } catch (rollbackError) {
        // Log rollback failure but don't mask original error
        console.error('Rollback failed:', rollbackError);
      }
    }
    throw error;
  }
}

Comment on lines +141 to +151
);
}
}

async findByPortPair(
sourcePortSystemId: number,
destPortSystemId: number,
fileSystemId: number,
): Promise<{
systemId: number;
isDeleted: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY][High] Race condition in duplicate link detection

The findByPortPair method in data-link.repository.ts performs a read operation to check for existing links, followed by a separate write operation in createDataLink. This creates a race condition window where two concurrent requests could both pass the duplicate check and create duplicate links.

Issue: Between the time findByPortPair returns null and createDataLink inserts the new row, another transaction could insert the same link. This violates the uniqueness constraint that the duplicate check is meant to enforce.

Impact:

  • Data integrity violation (duplicate links in database)
  • Inconsistent application state
  • Potential downstream errors when querying links
Fixed Code Snippet
// In data-link.repository.ts - Add database-level unique constraint
async createDataLink(
  dataLink: DataLink,
  boundaryPortPayloads: BoundaryPortPayload[],
  options?: EditOptions,
): Promise<void> {
  const {session, groupId} = this.uow.getWriteContext();
  const fileSystemId = dataLink.fileSystemId;
  const writer = this.requireWriter();

  // Use INSERT ... ON CONFLICT or equivalent to make the operation atomic
  try {
    // First, check with SELECT FOR UPDATE to lock the row if it exists
    const lockQuery = await this.manager.query(
      `SELECT system_id FROM data_links 
       WHERE source_port_system_id = $1 
       AND destination_port_system_id = $2 
       AND file_system_id = $3 
       FOR UPDATE NOWAIT`,
      [dataLink.sourcePortSystemId, dataLink.destinationPortSystemId, fileSystemId]
    );
    
    if (lockQuery.length > 0) {
      throw new ConflictException(
        `DataLink for ports already exists (detected during atomic insert)`
      );
    }

    // Proceed with insert operations...
    for (const bp of boundaryPortPayloads) {
      // ... existing code
    }
  } catch (error) {
    if (error.code === '23505') { // PostgreSQL unique violation
      throw new ConflictException(
        `DataLink for ports (${dataLink.sourcePortSystemId}, ${dataLink.destinationPortSystemId}) already exists.`
      );
    }
    throw error;
  }
}

Alternative Solution: Add a unique constraint at the database level on (source_port_system_id, destination_port_system_id, file_system_id) and handle the constraint violation exception.

Comment on lines +42 to +52
try {
const {session} = uow.getWriteContext();
const fileSystemId = session.fileSystemId;

const srcModuleId = Number.parseInt(command.sourceModuleSystemId, 10);
const dstModuleId = Number.parseInt(
command.destinationModuleSystemId,
10,
);
const srcPortId = Number.parseInt(command.sourcePortSystemId, 10);
const dstPortId = Number.parseInt(command.destinationPortSystemId, 10);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY][Medium] Missing validation for Number.parseInt conversions

In both CreateDataLinkHandler and CreateDataLinkWithSubsystemsHandler, string inputs are converted to numbers using Number.parseInt() without validating that the conversion succeeded. If invalid input is provided (e.g., non-numeric strings), parseInt returns NaN, which will cause downstream errors or unexpected behavior.

Issue:

const srcModuleId = Number.parseInt(command.sourceModuleSystemId, 10);
// If command.sourceModuleSystemId is 'abc', srcModuleId becomes NaN
// Subsequent database queries with NaN will fail or return unexpected results

Impact:

  • Cryptic error messages for users
  • Potential database errors
  • Inconsistent error handling
Fixed Code Snippet
// In create-data-link.handler.ts
const srcModuleId = Number.parseInt(command.sourceModuleSystemId, 10);
const dstModuleId = Number.parseInt(command.destinationModuleSystemId, 10);
const srcPortId = Number.parseInt(command.sourcePortSystemId, 10);
const dstPortId = Number.parseInt(command.destinationPortSystemId, 10);

if (isNaN(srcModuleId) || isNaN(dstModuleId) || isNaN(srcPortId) || isNaN(dstPortId)) {
  throw new DomainRuleViolationException([{
    code: 'INVALID_INPUT',
    message: 'All system IDs must be valid numeric values',
    severity: IssueSeverity.Error,
  }]);
}

Comment on lines +66 to +76
try {
const {session} = uow.getWriteContext();
const fileSystemId = session.fileSystemId;

const srcNodeId = Number.parseInt(command.sourceNodeSystemId, 10);
const dstNodeId = Number.parseInt(command.destinationNodeSystemId, 10);
const srcPortId = Number.parseInt(command.sourcePortSystemId, 10);
const dstPortId = Number.parseInt(command.destinationPortSystemId, 10);

// FR-DLS-04: self-loop check
if (srcNodeId === dstNodeId) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY][Medium] Missing validation for Number.parseInt conversions

In both CreateDataLinkHandler and CreateDataLinkWithSubsystemsHandler, string inputs are converted to numbers using Number.parseInt() without validating that the conversion succeeded. If invalid input is provided (e.g., non-numeric strings), parseInt returns NaN, which will cause downstream errors or unexpected behavior.

Issue:

const srcModuleId = Number.parseInt(command.sourceModuleSystemId, 10);
// If command.sourceModuleSystemId is 'abc', srcModuleId becomes NaN
// Subsequent database queries with NaN will fail or return unexpected results

Impact:

  • Cryptic error messages for users
  • Potential database errors
  • Inconsistent error handling
Fixed Code Snippet
// In create-data-link.handler.ts
const srcModuleId = Number.parseInt(command.sourceModuleSystemId, 10);
const dstModuleId = Number.parseInt(command.destinationModuleSystemId, 10);
const srcPortId = Number.parseInt(command.sourcePortSystemId, 10);
const dstPortId = Number.parseInt(command.destinationPortSystemId, 10);

if (isNaN(srcModuleId) || isNaN(dstModuleId) || isNaN(srcPortId) || isNaN(dstPortId)) {
  throw new DomainRuleViolationException([{
    code: 'INVALID_INPUT',
    message: 'All system IDs must be valid numeric values',
    severity: IssueSeverity.Error,
  }]);
}

Comment on lines +102 to 110
* Create a new data link (collapsed view, module endpoints only).
* Stores all link segments in DB; returns ComponentsResponseDto.
*/
@Post()
@ApiDocumentationWithExample({
summary: 'Create a new data link (flat view)',
summary: 'Create a new data link',
description:
'Creates a data link between two modules. Stores all segments (mod→SS, SS→SS, SS→mod) in the DB. ' +
'Returns a flat ComponentsResponseDto with the created link.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MAINTAINABILITY][Medium] Inconsistent error handling for subsystem vs module type checks

In CreateDataLinkHandler.handle(), when a node is found to be a subsystem instead of a module, the code throws a DomainRuleViolationException with code WRONG_NODE_TYPE. However, in the controller layer (data-link.controller.ts), this same scenario is documented as returning 404 (Not Found) in some cases and 422 (Unprocessable Entity) in others.

Issue: The error handling strategy is inconsistent:

  1. Handler throws DomainRuleViolationException (maps to 422)
  2. Documentation suggests 404 for "module not found"
  3. The distinction between "node doesn't exist" vs "node exists but wrong type" is important for API consumers

Current behavior:

if (srcModule === null) {
  const isSubsystem = await subsystemRepo.subsystemExists(srcModuleId, fileSystemId);
  if (isSubsystem) {
    throw new DomainRuleViolationException([{  // Returns 422
      code: 'WRONG_NODE_TYPE',
      message: `Source node ${BinaryUtils.toHexString(srcModuleId)} is a subsystem, not a module.`,
      severity: IssueSeverity.Error,
    }]);
  }
  throw new ResourceNotFoundException(  // Returns 404
    `Source module ${BinaryUtils.toHexString(srcModuleId)} not found.`,
  );
}

Recommendation: This is actually correct behavior - returning 422 for wrong type and 404 for not found is semantically appropriate. However, the API documentation in the controller should be updated to clearly reflect this distinction.

Fixed Code Snippet
// In data-link.controller.ts - Update API documentation
@ApiDocumentationWithExample({
  summary: 'Create a new data link',
  description:
    'Creates a data link between two modules. Stores all segments (mod→SS, SS→SS, SS→mod) in the DB. ' +
    'Returns a flat ComponentsResponseDto with the created link.',
  requestDto: CreateDataLinkRequest,
  requestDtoDescription: 'Data link creation parameters',
  responses: [
    {
      status: HttpStatus.CREATED,
      description: 'Data link created successfully',
      dto: ComponentsResponseDto,
    },
    {status: HttpStatus.BAD_REQUEST, description: 'Invalid request data (malformed input)'},
    {
      status: HttpStatus.NOT_FOUND,
      description: 'Source or destination module does not exist in the file',
    },
    {
      status: HttpStatus.UNPROCESSABLE_ENTITY,
      description: 'Validation failed: wrong node type (subsystem instead of module), wrong port direction, port ownership mismatch, self-loop, or other business rule violation',
    },
    {
      status: HttpStatus.CONFLICT,
      description: 'Data link already exists for the given port pair',
    },
    {
      status: HttpStatus.INTERNAL_SERVER_ERROR,
      description: 'Failed to create data link',
    },
  ],
})

@aboppay
aboppay force-pushed the set-data-links branch 2 times, most recently from 4c26fb6 to 1cf3a44 Compare August 23, 2026 17:24
@aboppay aboppay changed the title docs: add post data links API requirements feat: implement create-data link APIs Aug 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files Not Fully Analyzed

  • docs/data-links/design/data-links-post-design.md - File extension .md not supported
  • packages/api/src/infrastructure-wrapper/filters/all-exceptions.filter.ts - Full file content skipped due to token limit
  • packages/api/src/infrastructure-wrapper/persistence/unit-of-work/typeorm-unit-of-work.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/swagger-doc/dto-examples/usecase-api-examples.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/data-link.controller.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-request.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-with-subsystems-request.dto.ts - Full file content skipped due to token limit
  • packages/api/tests/e2e/data-links/create-data-link.e2e-spec.ts - Full file content skipped due to token limit
  • packages/core/src/application/file-operations/upload-file/services/entity-builders/subsystem-builder.ts - Full file content skipped due to token limit
  • packages/core/src/application/orchestration/cqrs/registries/command-handler-registry.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/subsystem/subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/usecase-components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/build-traversal-entities.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.handler.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/dto/component-collection-dto.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/build-subsystem-tree.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.ts - Full file content skipped due to token limit
  • packages/core/src/domain/services/subsystem-data-links/subsystem-data-link-derivation.service.ts - Full file content skipped due to token limit
  • packages/core/src/index.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/conflict.exception.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/index.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/file-operations/upload-file/services/entity-builders/subsystem-builder.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/domain/services/subsystem-links/subsystem-data-link-derivation.service.spec.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/subsystem/db-subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/tests/integration/repositories/data-link/data-link-edit.repository.integration.spec.ts - Full file content skipped due to token limit

Qualcomm AI Review

Comment on lines +336 to +346
dataLink.destinationPortSystemId,
dataLink.linkType,
dataLink.isEc,
);
} catch (error) {
if (uow.isInTransaction()) await uow.rollback();
throw error;
}
}

private deriveLinkType(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functionality - High] Missing Transaction Rollback in Nested Error Paths

In CreateDataLinkHandler.handle() and CreateDataLinkWithSubsystemsHandler.handle(), the error handling catch block only rolls back if uow.isInTransaction() returns true. However, there are multiple validation checks (self-loop, port direction, etc.) that throw exceptions before the transaction is started. If an exception is thrown after startTransaction() but before entering the try block's main logic, the rollback may not execute properly.

Additionally, in the re-activation path (when existing.isDeleted is true), if buildTraversalEntities() or createDataLink() fails, the transaction remains open because the error is thrown before the commit, but the catch block may not properly handle all error types.

Impact: Database connections could be left in an inconsistent state, potentially causing connection pool exhaustion under load.

Fixed Code Snippet
async handle(
  command: CreateDataLinkCommand,
): Promise<ComponentCollectionDto> {
  const uow = this.uow;
  try {
    await uow.startTransaction();
    const {session} = uow.getWriteContext();
    const fileSystemId = session.fileSystemId;

    // ... validation and processing logic ...

    await uow.commit();
    return this.buildDto(/* ... */);
  } catch (error) {
    // Always attempt rollback if transaction was started
    if (uow.isInTransaction()) {
      try {
        await uow.rollback();
      } catch (rollbackError) {
        // Log rollback failure but throw original error
        console.error('Transaction rollback failed:', rollbackError);
      }
    }
    throw error;
  }
}

Comment on lines +90 to +100

return (row?.dp_port_io_type as PortIoType) ?? null;
}

async isPortOccupiedAsSource(
portSystemId: number,
fileSystemId: number,
): Promise<boolean> {
const count = await this.manager
.createQueryBuilder()
.select('1')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functionality - High] Race Condition in Port Occupation Validation

In CreateDataLinkWithSubsystemsHandler.handleBranchB(), the port occupation checks (isPortOccupiedAsSource and isPortOccupiedAsDest) are performed as separate queries before the actual SLS creation. This creates a race condition window where two concurrent requests could both pass the validation checks and then both create conflicting SLS segments.

Scenario:

  1. Request A checks if port 401 is occupied as source → returns false
  2. Request B checks if port 401 is occupied as source → returns false (before A commits)
  3. Request A creates SLS with port 401 as source
  4. Request B creates SLS with port 401 as source (violates FR-DLS-07)

Impact: Data integrity violation - subsystem ports could end up with multiple connections despite the single-connection constraint.

Fixed Code Snippet
// In subsystem.repository.ts, add atomic check-and-insert
async checkAndReservePort(
  portSystemId: number,
  direction: 'source' | 'dest',
  fileSystemId: number,
  slsSystemId: number,
): Promise<boolean> {
  const column = direction === 'source' ? 'source_port_system_id' : 'destination_port_system_id';
  
  // Use INSERT with SELECT to atomically check and reserve
  const result = await this.manager.query(
    `INSERT INTO subsystem_data_links (system_id, ${column}, file_system_id, data_link_system_id)
     SELECT ?, ?, ?, NULL
     WHERE NOT EXISTS (
       SELECT 1 FROM subsystem_data_links 
       WHERE ${column} = ? AND file_system_id = ?
     )`,
    [slsSystemId, portSystemId, fileSystemId, portSystemId, fileSystemId]
  );
  
  return result.affectedRows > 0;
}

Comment on lines +62 to +72
]);
}

// FR-DL-02/03: module existence and type check
const moduleRepo = uow.getModuleRepository();
const subsystemRepo = uow.getSubsystemRepository();
const [srcModule, dstModule] = await Promise.all([
moduleRepo.findModulePortsForLink(srcModuleId, fileSystemId),
moduleRepo.findModulePortsForLink(dstModuleId, fileSystemId),
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Performance - Medium] Inefficient Sequential Database Queries

In CreateDataLinkHandler.handle(), the module validation performs two separate findModulePortsForLink() calls sequentially, followed by two more subsystemExists() calls if validation fails. This results in 2-4 database round trips that could be optimized.

Similarly, in the subgraph validation for INTER_USECASE links, two sequential getUsecaseSystemIdForSubgraph() calls are made.

Impact: Increased latency for link creation operations, especially noticeable under high load or with network latency to the database.

Fixed Code Snippet
// Create a batch query method in ModuleRepository
async findMultipleModulePortsForLink(
  moduleSystemIds: number[],
  fileSystemId: number,
): Promise<Map<number, {subgraphSystemId: number; ports: {systemId: number; portIoType: PortIoType}[]}>> {
  const sessionId = this.uow.getWriteContext().session.sessionId;
  const results = new Map();
  
  // Fetch all modules in a single query
  const moduleNodes = await this.moduleNodeFetcher.fetchMultiple(
    moduleSystemIds,
    fileSystemId,
    sessionId,
  );
  
  // Fetch all ports in a single query
  const allPorts = await this.portFetcher.fetchDataPortsForMultipleNodes(
    moduleSystemIds,
    fileSystemId,
    sessionId,
  );
  
  for (const [moduleId, node] of moduleNodes) {
    results.set(moduleId, {
      subgraphSystemId: node.subgraphSystemId,
      ports: allPorts.get(moduleId) || [],
    });
  }
  
  return results;
}

// In handler:
const modulesData = await moduleRepo.findMultipleModulePortsForLink(
  [srcModuleId, dstModuleId],
  fileSystemId,
);
const srcModule = modulesData.get(srcModuleId) ?? null;
const dstModule = modulesData.get(dstModuleId) ?? null;

Comment on lines +153 to +163
} | null> {
const {session} = this.uow.getWriteContext();
const sessionId = session.sessionId;

const baseRow = await this.manager
.createQueryBuilder()
.select('dl.systemId')
.from(ENTITY_NAMES.DataLink, 'dl')
.where(
'dl.sourcePortSystemId = :srcPort AND dl.destinationPortSystemId = :dstPort AND dl.fileSystemId = :fileSystemId',
{srcPort: sourcePortSystemId, dstPort: destPortSystemId, fileSystemId},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Performance - Medium] Missing Database Index Validation

The findByPortPair() method in TypeOrmDataLinkRepository queries the data_links table using a composite WHERE clause on sourcePortSystemId, destinationPortSystemId, and fileSystemId. However, there's no verification that an appropriate composite index exists on these columns.

Without a proper index, this query will perform a full table scan as the number of data links grows, severely impacting performance for duplicate detection (FR-DL-07).

Impact: O(n) query performance instead of O(log n), causing significant slowdown as the number of links increases.

Fixed Code Snippet
// Add migration to create composite index
// In a new migration file:
export class AddDataLinkPortPairIndex1234567890 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `CREATE INDEX idx_data_links_port_pair 
       ON data_links (source_port_system_id, destination_port_system_id, file_system_id)`
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP INDEX idx_data_links_port_pair`);
  }
}

// Add index hint in repository query:
const baseRow = await this.manager
  .createQueryBuilder()
  .select('dl.systemId')
  .from(ENTITY_NAMES.DataLink, 'dl')
  .where(
    'dl.sourcePortSystemId = :srcPort AND dl.destinationPortSystemId = :dstPort AND dl.fileSystemId = :fileSystemId',
    {srcPort: sourcePortSystemId, dstPort: destPortSystemId, fileSystemId},
  )
  .useIndex('idx_data_links_port_pair')  // Add index hint
  .getRawOne<{dl_system_id: number}>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files Not Fully Analyzed

  • docs/data-links/design/data-links-post-design.md - File extension .md not supported
  • docs/data-links/requirements/data-links-post-requirements.md - File extension .md not supported
  • packages/api/src/infrastructure-wrapper/filters/all-exceptions.filter.ts - Full file content skipped due to token limit
  • packages/api/src/infrastructure-wrapper/persistence/unit-of-work/typeorm-unit-of-work.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/swagger-doc/dto-examples/usecase-api-examples.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/data-link.controller.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-request.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-with-subsystems-request.dto.ts - Full file content skipped due to token limit
  • packages/api/tests/e2e/data-links/create-data-link.e2e-spec.ts - Full file content skipped due to token limit
  • packages/core/src/application/file-operations/upload-file/services/entity-builders/subsystem-builder.ts - Full file content skipped due to token limit
  • packages/core/src/application/orchestration/cqrs/registries/command-handler-registry.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/subsystem/subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/usecase-components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/build-traversal-entities.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.handler.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/dto/component-collection-dto.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/build-subsystem-tree.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.ts - Full file content skipped due to token limit
  • packages/core/src/domain/services/subsystem-data-links/subsystem-data-link-derivation.service.ts - Full file content skipped due to token limit
  • packages/core/src/index.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/conflict.exception.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/index.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/file-operations/upload-file/services/entity-builders/subsystem-builder.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/domain/services/subsystem-links/subsystem-data-link-derivation.service.spec.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/subsystem/db-subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/tests/integration/repositories/data-link/data-link-edit.repository.integration.spec.ts - Full file content skipped due to token limit

Qualcomm AI Review

Comment on lines +336 to +346
dataLink.destinationPortSystemId,
dataLink.linkType,
dataLink.isEc,
);
} catch (error) {
if (uow.isInTransaction()) await uow.rollback();
throw error;
}
}

private deriveLinkType(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Security/Functionality - High] Missing transaction rollback in error handling paths

In both CreateDataLinkHandler and CreateDataLinkWithSubsystemsHandler, the error handling catch block checks uow.isInTransaction() before rolling back. However, if an error occurs after uow.commit() is called but before the method returns, the transaction state may be inconsistent. Additionally, if uow.rollback() itself throws an error, the original error will be masked.

Issue Details:

  • If commit fails partially, the rollback may not execute
  • Nested errors in rollback can mask the original error
  • No logging of rollback failures

Impact: Data corruption, partial commits, difficult debugging

Fixed Code Snippet
async handle(
  command: CreateDataLinkCommand,
): Promise<ComponentCollectionDto> {
  const uow = this.uow;
  await uow.startTransaction();
  try {
    // ... existing logic ...
    await uow.commit();
    return this.buildDto(/* ... */);
  } catch (error) {
    try {
      if (uow.isInTransaction()) {
        await uow.rollback();
      }
    } catch (rollbackError) {
      // Log rollback failure but throw original error
      console.error('Rollback failed:', rollbackError);
    }
    throw error;
  }
}

Comment on lines 45 to +55
sessionId,
);
}

async createDataLink(
dataLink: DataLink,
boundaryPortPayloads: BoundaryPortPayload[],
options?: EditOptions,
): Promise<void> {
const {session, groupId} = this.uow.getWriteContext();
const fileSystemId = dataLink.fileSystemId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Functionality - High] Race condition in duplicate link detection

The findByPortPair method in DataLinkRepository checks for existing links, but there's a time window between the check and the actual creation where another concurrent request could create the same link. This is a classic TOCTOU (Time-of-Check-Time-of-Use) race condition.

Issue Details:

  • Two simultaneous requests with identical port pairs can both pass the duplicate check
  • Both requests will attempt to create the link, potentially causing database constraint violations or duplicate data
  • The session-based overlay doesn't prevent this race at the database level

Impact: Duplicate links, constraint violations, data inconsistency

Recommendation: Add a unique constraint at the database level on (source_port_system_id, destination_port_system_id, file_system_id) and handle the constraint violation gracefully by converting it to a ConflictException.

Fixed Code Snippet
async createDataLink(
  dataLink: DataLink,
  boundaryPortPayloads: BoundaryPortPayload[],
  options?: EditOptions,
): Promise<void> {
  const {session, groupId} = this.uow.getWriteContext();
  const fileSystemId = dataLink.fileSystemId;
  const writer = this.requireWriter();

  try {
    // ... existing creation logic ...
  } catch (error: any) {
    // Handle unique constraint violation
    if (error.code === 'SQLITE_CONSTRAINT' || error.message?.includes('UNIQUE constraint')) {
      throw new ConflictException(
        `DataLink for ports (${dataLink.sourcePortSystemId}, ${dataLink.destinationPortSystemId}) already exists.`
      );
    }
    throw error;
  }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files Not Fully Analyzed

  • docs/data-links/design/data-links-post-design.md - File extension .md not supported
  • docs/data-links/requirements/data-links-post-requirements.md - File extension .md not supported
  • packages/api/src/infrastructure-wrapper/filters/all-exceptions.filter.ts - Full file content skipped due to token limit
  • packages/api/src/infrastructure-wrapper/persistence/unit-of-work/typeorm-unit-of-work.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/swagger-doc/dto-examples/usecase-api-examples.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/data-link.controller.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-request.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-with-subsystems-request.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/response/subsystem-data-link-response.dto.ts - Full file content skipped due to token limit
  • packages/api/tests/e2e/data-links/create-data-link.e2e-spec.ts - Full file content skipped due to token limit
  • packages/core/src/application/file-operations/upload-file/services/entity-builders/subsystem-builder.ts - Full file content skipped due to token limit
  • packages/core/src/application/orchestration/cqrs/registries/command-handler-registry.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/subsystem/subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/subsystem-data-link-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/usecase-components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/build-traversal-entities.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/dto/component-collection-dto.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/build-subsystem-tree.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.ts - Full file content skipped due to token limit
  • packages/core/src/domain/services/subsystem-data-links/subsystem-data-link-derivation.service.ts - Full file content skipped due to token limit
  • packages/core/src/index.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/conflict.exception.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/index.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/file-operations/upload-file/services/entity-builders/subsystem-builder.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/domain/services/subsystem-links/subsystem-data-link-derivation.service.spec.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/subsystem/db-subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/tests/integration/repositories/data-link/data-link-edit.repository.integration.spec.ts - Full file content skipped due to token limit

Qualcomm AI Review

Comment on lines +231 to +241
dataLink.destinationPortSystemId,
dataLink.linkType,
dataLink.isEc,
);
} catch (error) {
if (uow.isInTransaction()) await uow.rollback();
throw error;
}
}

private async findModules(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY] Missing Transaction Rollback in Catch Block - High Severity

In both CreateDataLinkHandler and CreateDataLinkWithSubsystemsHandler, the catch block checks uow.isInTransaction() before rolling back. However, if an error occurs after startTransaction() but the transaction state becomes inconsistent, the rollback may not execute, leaving the transaction open and potentially causing connection pool exhaustion.

Issue Location: The pattern appears in both handlers:

catch (error) {
  if (uow.isInTransaction()) await uow.rollback();
  throw error;
}

The problem is that isInTransaction() might return false even when a transaction was started, if the error corrupted the transaction state. This could lead to leaked database connections.

Fixed Code Snippet
catch (error) {
  try {
    await uow.rollback();
  } catch (rollbackError) {
    // Log rollback failure but don't mask original error
    console.error('Failed to rollback transaction:', rollbackError);
  }
  throw error;
}

Always attempt rollback unconditionally in the catch block. The rollback operation itself should be idempotent and safe to call even if no transaction is active.

Comment on lines +60 to +70
): Promise<PortIoType | null> {
const sessionId = this.uow.getWriteContext().session.sessionId;

// Check session overlay first — a staged CREATE wins over the base table
const actions = await this.editActionsQs.getByTable(
sessionId,
ENTITY_NAMES.DataPort,
);
for (const action of actions) {
if (
action.operation === CHANGE_OPERATION.Create &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[PERFORMANCE] Inefficient Sequential Edit Action Queries - Medium Severity

In TypeOrmSubsystemRepository.getPortIoType(), the method calls editActionsQs.getByTable() which loads ALL edit actions for the DataPort table, then iterates through them to find a match. This is inefficient when there are many staged changes, as it loads unnecessary data into memory.

Issue Location:

const actions = await this.editActionsQs.getByTable(
  sessionId,
  ENTITY_NAMES.DataPort,
);
for (const action of actions) {
  if (
    action.operation === CHANGE_OPERATION.Create &&
    action.targetSystemId === portSystemId
  ) {
    // ...
  }
}

This pattern loads all DataPort edit actions when we only need actions for a specific portSystemId.

Fixed Code Snippet
// Add a new method to EditActionsQueryService:
async getByTableAndSystemId(
  sessionId: number,
  tableName: string,
  targetSystemId: number
): Promise<EditActionRow[]> {
  return this.manager
    .createQueryBuilder()
    .select('ea')
    .from('edit_actions', 'ea')
    .where('ea.session_id = :sessionId', { sessionId })
    .andWhere('ea.target_table = :tableName', { tableName })
    .andWhere('ea.target_system_id = :targetSystemId', { targetSystemId })
    .andWhere('ea.valid_until IS NULL')
    .orderBy('ea.change_id', 'ASC')
    .getRawMany();
}

// Then use it in getPortIoType:
const actions = await this.editActionsQs.getByTableAndSystemId(
  sessionId,
  ENTITY_NAMES.DataPort,
  portSystemId
);

This reduces the data transferred and processed, especially important when there are many staged changes in the session.

Comment on lines +204 to +214

return null;
}

async reactivateDataLink(
systemId: number,
aggregateId: number,
payload: Record<string, unknown>,
options?: EditOptions,
): Promise<void> {
const {session, groupId} = this.uow.getWriteContext();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SECURITY] Raw SQL Query Without Explicit Parameterization Validation - Medium Severity

In TypeOrmDataLinkRepository.reactivateDataLink(), a raw SQL query is used with template literals. While the parameters appear to be properly passed as an array, the comment indicates this is necessary because TypeORM QueryBuilder cannot express the required logic. However, there's no explicit validation that the systemId parameter is actually a number before it's interpolated.

Issue Location:

await this.manager.query(
  `UPDATE edit_actions SET valid_until = $1 WHERE session_id = $2 AND target_system_id = $3 AND field_path IS NULL AND valid_until IS NULL`,
  [new Date().toISOString(), session.sessionId, systemId],
);

While the parameters are passed as an array (which is safe), the method signature accepts systemId: number but doesn't validate it's actually a number at runtime. If called with a non-number due to a type system bypass, this could cause issues.

Fixed Code Snippet
async reactivateDataLink(
  systemId: number,
  aggregateId: number,
  payload: Record<string, unknown>,
  options?: EditOptions,
): Promise<void> {
  // Validate inputs are actually numbers
  if (!Number.isInteger(systemId) || !Number.isInteger(aggregateId)) {
    throw new Error('systemId and aggregateId must be integers');
  }
  
  const {session, groupId} = this.uow.getWriteContext();
  const writer = this.requireWriter();
  
  await this.manager.query(
    `UPDATE edit_actions SET valid_until = $1 WHERE session_id = $2 AND target_system_id = $3 AND field_path IS NULL AND valid_until IS NULL`,
    [new Date().toISOString(), session.sessionId, systemId],
  );
  
  await writer.writeCreate(
    {
      targetTable: ENTITY_NAMES.DataLink,
      targetSystemId: systemId,
      aggregateId,
      payload,
      ...options,
    },
    session.sessionId,
    groupId,
    this.manager,
  );
}

Add runtime validation to ensure type safety even if TypeScript's type system is bypassed (e.g., through any casts elsewhere in the codebase).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files Not Fully Analyzed

  • docs/data-links/design/data-links-post-design.md - File extension .md not supported
  • docs/data-links/requirements/data-links-post-requirements.md - File extension .md not supported
  • packages/api/src/infrastructure-wrapper/filters/all-exceptions.filter.ts - Full file content skipped due to token limit
  • packages/api/src/infrastructure-wrapper/persistence/unit-of-work/typeorm-unit-of-work.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/dto/component-collection-with-subsystems.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/dto/subsystem-components-response.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/swagger-doc/dto-examples/usecase-api-examples.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/data-link.controller.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-request.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-with-subsystems-request.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/response/subsystem-data-link-response.dto.ts - Full file content skipped due to token limit
  • packages/core/src/application/file-operations/upload-file/services/entity-builders/subsystem-builder.ts - Full file content skipped due to token limit
  • packages/core/src/application/orchestration/cqrs/registries/command-handler-registry.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/subsystem/subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/subsystem-data-link-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/usecase-components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/build-traversal-entities.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.handler.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/dto/component-collection-dto.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/build-subsystem-tree.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.ts - Full file content skipped due to token limit
  • packages/core/src/domain/services/subsystem-data-links/subsystem-data-link-derivation.service.ts - Full file content skipped due to token limit
  • packages/core/src/index.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/index.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/file-operations/upload-file/services/entity-builders/subsystem-builder.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/usecase/get-component-with-subsystem/build-subsystem-tree.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/domain/services/subsystem-links/subsystem-data-link-derivation.service.spec.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/subsystem/db-subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/tests/integration/repositories/data-link/data-link-edit.repository.integration.spec.ts - Full file content skipped due to token limit

Qualcomm AI Review

Comment on lines +90 to +100

return (row?.dp_port_io_type as PortIoType) ?? null;
}

async isPortOccupiedAsSource(
portSystemId: number,
fileSystemId: number,
): Promise<boolean> {
const count = await this.manager
.createQueryBuilder()
.select('1')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High Severity - Functionality] Race condition in subsystem port occupation checks

In CreateDataLinkWithSubsystemsHandler.handleBranchB(), the port occupation checks (isPortOccupiedAsSource and isPortOccupiedAsDest) are performed before the transaction commits. However, these checks query the base table only and don't account for concurrent transactions that might be creating SLS segments with the same ports. This could allow two concurrent requests to pass validation and both create links using the same subsystem port, violating the single-occupancy constraint (FR-DLS-07).

Impact: Data integrity violation - multiple SLS segments could reference the same subsystem boundary port as source or destination, breaking the architectural constraint that each subsystem port can only be used once.

Recommendation:

  1. Add row-level locking when checking port occupation (SELECT FOR UPDATE)
  2. Perform the occupation check within the same transaction as the write
  3. Consider adding a unique constraint at the database level as a safety net
Fixed Code Snippet
// In subsystem.repository.ts
async isPortOccupiedAsSource(
  portSystemId: number,
  fileSystemId: number,
): Promise<boolean> {
  // Add FOR UPDATE to lock the rows during check
  const count = await this.manager
    .createQueryBuilder()
    .select('1')
    .from(ENTITY_NAMES.SubsystemDataLink, 'sls')
    .where(
      'sls.sourcePortSystemId = :portSystemId AND sls.fileSystemId = :fileSystemId',
      {portSystemId, fileSystemId},
    )
    .setLock('pessimistic_write') // Add row lock
    .getCount();
  return count > 0;
}

Comment on lines +117 to +127
fileSystemId,
uow,
subsystemRepo,
);
} catch (error) {
if (uow.isInTransaction()) await uow.rollback();
throw error;
}
}

private async handleBranchA(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High Severity - Error Handling] Missing transaction rollback in catch block

In CreateDataLinkWithSubsystemsHandler.handle(), the catch block checks uow.isInTransaction() before rolling back. However, if an error occurs during uow.commit(), the transaction might be in an indeterminate state where isInTransaction() returns false but the transaction hasn't been properly cleaned up. This could lead to connection leaks or inconsistent database state.

Impact: Resource leaks and potential database inconsistencies when errors occur during commit phase.

Recommendation: Always attempt rollback in the catch block regardless of transaction state, and handle any rollback errors gracefully. The database driver should handle the case where no transaction is active.

Fixed Code Snippet
async handle(
  command: CreateDataLinkWithSubsystemsCommand,
): Promise<ComponentCollectionWithSubsystemsDto> {
  const uow = this.uow;
  await uow.startTransaction();
  try {
    // ... handler logic ...
    await uow.commit();
    return result;
  } catch (error) {
    // Always attempt rollback, let the UoW handle transaction state
    try {
      await uow.rollback();
    } catch (rollbackError) {
      // Log rollback failure but throw original error
      console.error('Rollback failed:', rollbackError);
    }
    throw error;
  }
}

Comment on lines +297 to +307
await uow.commit();
return emptyCollection(slsSegments.map(sls => mapSubsystemDataLink(sls)));
}

private async findModules(
moduleRepo: ReturnType<UnitOfWork['getModuleRepository']>,
subsystemRepo: ReturnType<UnitOfWork['getSubsystemRepository']>,
srcNodeId: number,
dstNodeId: number,
fileSystemId: number,
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium Severity - Performance] Sequential database queries in module validation

In CreateDataLinkWithSubsystemsHandler.findModules(), the method performs two sequential findModulePortsForLink() calls followed by two more sequential subsystemExists() calls in the error paths. This results in up to 4 sequential database round-trips when both modules are invalid. These queries could be batched or parallelized to reduce latency.

Impact: Increased response time for validation failures, especially noticeable under high load or with network latency to the database.

Recommendation: Use Promise.all() for the subsystem existence checks in error paths, similar to how the module lookups are already parallelized.

Fixed Code Snippet
private async findModules(
  moduleRepo: ReturnType<UnitOfWork['getModuleRepository']>,
  subsystemRepo: ReturnType<UnitOfWork['getSubsystemRepository']>,
  srcNodeId: number,
  dstNodeId: number,
  fileSystemId: number,
) {
  const [srcModule, dstModule] = await Promise.all([
    moduleRepo.findModulePortsForLink(srcNodeId, fileSystemId),
    moduleRepo.findModulePortsForLink(dstNodeId, fileSystemId),
  ]);
  
  // Batch subsystem checks if either module is null
  if (srcModule === null || dstModule === null) {
    const [srcIsSubsystem, dstIsSubsystem] = await Promise.all([
      srcModule === null ? subsystemRepo.subsystemExists(srcNodeId, fileSystemId) : Promise.resolve(false),
      dstModule === null ? subsystemRepo.subsystemExists(dstNodeId, fileSystemId) : Promise.resolve(false),
    ]);
    
    if (srcModule === null) {
      throw srcIsSubsystem
        ? new DomainRuleViolationException([/* ... */])
        : new ResourceNotFoundException(/* ... */);
    }
    if (dstModule === null) {
      throw dstIsSubsystem
        ? new DomainRuleViolationException([/* ... */])
        : new ResourceNotFoundException(/* ... */);
    }
  }
  
  return [srcModule, dstModule] as const;
}

Comment on lines +417 to +427
]);
}

// FR-DLS-03 + FR-DLS-08 + FR-DLS-07: validate subsystem-side ports
if (srcIsSubsystem) {
const srcPortType = await subsystemRepo.getPortIoType(
srcPortId,
fileSystemId,
);
if (srcPortType === null) {
throw new ResourceNotFoundException(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium Severity - Error Handling] Incomplete validation of port ownership in subsystem branch

In CreateDataLinkWithSubsystemsHandler.handleBranchB(), when validating subsystem ports, the code checks getPortIoType() and throws ResourceNotFoundException if null. However, this doesn't distinguish between a port that doesn't exist at all versus a port that exists but belongs to a different node. The error message "Source port not found" is misleading if the port exists but is attached to the wrong node.

Impact: Confusing error messages for API consumers when they provide a valid port ID that belongs to a different node than specified.

Recommendation: Add an explicit check using portExists() first, then verify ownership, similar to the pattern used in findPort() for Branch A.

Fixed Code Snippet
if (srcIsSubsystem) {
  // First check if port exists at all
  const portExists = await subsystemRepo.portExists(srcPortId, fileSystemId);
  if (!portExists) {
    throw new ResourceNotFoundException(
      `Source port ${BinaryUtils.toHexString(srcPortId)} not found.`,
    );
  }
  
  // Then check port type
  const srcPortType = await subsystemRepo.getPortIoType(
    srcPortId,
    fileSystemId,
  );
  if (srcPortType !== PORT_IO_TYPE.InputOutput) {
    throw new DomainRuleViolationException([
      {
        code: 'WRONG_SUBSYSTEM_PORT_TYPE',
        message: `Source subsystem port must be InputOutput, got ${srcPortType}.`,
        severity: IssueSeverity.Error,
      },
    ]);
  }
  
  // Finally check occupation
  const occupied = await subsystemRepo.isPortOccupiedAsSource(
    srcPortId,
    fileSystemId,
  );
  if (occupied) {
    throw new DomainRuleViolationException([
      {
        code: 'PORT_ALREADY_OCCUPIED',
        message: `Source port ${BinaryUtils.toHexString(srcPortId)} is already occupied as source of an SLS.`,
        severity: IssueSeverity.Error,
      },
    ]);
  }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files Not Fully Analyzed

  • docs/data-links/design/data-links-post-design.md - File extension .md not supported
  • docs/data-links/requirements/data-links-post-requirements.md - File extension .md not supported
  • packages/api/src/infrastructure-wrapper/filters/all-exceptions.filter.ts - Full file content skipped due to token limit
  • packages/api/src/infrastructure-wrapper/persistence/unit-of-work/typeorm-unit-of-work.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/dto/component-collection-with-subsystems.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/dto/subsystem-components-response.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/swagger-doc/dto-examples/usecase-api-examples.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/data-link.controller.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-request.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-with-subsystems-request.dto.ts - Full file content skipped due to token limit
  • packages/core/src/application/file-operations/upload-file/services/entity-builders/subsystem-builder.ts - Full file content skipped due to token limit
  • packages/core/src/application/orchestration/cqrs/registries/command-handler-registry.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/subsystem/subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/subsystem-data-link-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/usecase-components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/build-traversal-entities.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.handler.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/dto/component-collection-dto.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/build-subsystem-tree.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.ts - Full file content skipped due to token limit
  • packages/core/src/domain/services/subsystem-data-links/subsystem-data-link-derivation.service.ts - Full file content skipped due to token limit
  • packages/core/src/index.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/conflict.exception.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/index.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/file-operations/upload-file/services/entity-builders/subsystem-builder.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/usecase/get-component-with-subsystem/build-subsystem-tree.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/domain/services/subsystem-links/subsystem-data-link-derivation.service.spec.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/subsystem/db-subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/tests/integration/repositories/data-link/data-link-edit.repository.integration.spec.ts - Full file content skipped due to token limit

Qualcomm AI Review

Comment on lines 33 to +43
private readonly uow: UnitOfWork,
private readonly queryServices: QueryServices,
private readonly idGeneration: IdGenerationPort,
) {}

handle(_command: CreateDataLinkCommand): Promise<ComponentCollectionDto> {
async handle(
command: CreateDataLinkCommand,
): Promise<ComponentCollectionDto> {
const uow = this.uow;
await uow.startTransaction();
try {
const {session} = uow.getWriteContext();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY][High] Missing transaction rollback in error handling paths

In both CreateDataLinkHandler and CreateDataLinkWithSubsystemsHandler, the error handling catch block checks uow.isInTransaction() before rolling back. However, if an error occurs after startTransaction() but the transaction state is somehow not properly tracked, the rollback won't execute, potentially leaving the database in an inconsistent state.

Additionally, the handlers don't handle the case where commit() itself might fail. If commit() throws an error, the transaction remains open without cleanup.

Impact: Database inconsistency, resource leaks, and potential deadlocks in concurrent scenarios.

Fixed Code Snippet
async handle(
  command: CreateDataLinkCommand,
): Promise<ComponentCollectionDto> {
  const uow = this.uow;
  let transactionStarted = false;
  try {
    await uow.startTransaction();
    transactionStarted = true;
    
    // ... existing logic ...
    
    await uow.commit();
    transactionStarted = false;
    return this.buildDto(/* ... */);
  } catch (error) {
    if (transactionStarted) {
      try {
        await uow.rollback();
      } catch (rollbackError) {
        // Log rollback failure but throw original error
        console.error('Rollback failed:', rollbackError);
      }
    }
    throw error;
  }
}

Comment on lines 45 to +55
sessionId,
);
}

async createDataLink(
dataLink: DataLink,
boundaryPortPayloads: BoundaryPortPayload[],
options?: EditOptions,
): Promise<void> {
const {session, groupId} = this.uow.getWriteContext();
const fileSystemId = dataLink.fileSystemId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY][High] Race condition in duplicate link detection

The findByPortPair check and subsequent createDataLink call are not atomic. In a concurrent scenario, two requests could both pass the duplicate check and then both attempt to create the same link, potentially violating database constraints or creating duplicate records.

The time window between checking for duplicates (line 97-106 in create-data-link.handler.ts) and actually creating the link (line 223) allows for race conditions.

Impact: Duplicate data links could be created in high-concurrency scenarios, violating business rules and data integrity.

Recommendation: Implement optimistic locking or use database-level unique constraints with proper error handling. Alternatively, use a distributed lock or database transaction isolation level that prevents phantom reads.

Fixed Code Snippet
// In DataLinkRepository implementation
async createDataLink(
  dataLink: DataLink,
  boundaryPortPayloads: BoundaryPortPayload[],
  options?: EditOptions,
): Promise<void> {
  const {session, groupId} = this.uow.getWriteContext();
  const fileSystemId = dataLink.fileSystemId;
  const writer = this.requireWriter();

  // Check for duplicate immediately before write within same transaction
  const existing = await this.findByPortPair(
    dataLink.sourcePortSystemId,
    dataLink.destinationPortSystemId,
    fileSystemId,
  );
  
  if (existing !== null && !existing.isDeleted) {
    throw new ConflictException(
      `DataLink for ports already exists (detected during write)`,
    );
  }

  // Continue with creation...
  for (const bp of boundaryPortPayloads) {
    // ... existing code
  }
}

Alternatively, add a unique constraint at the database level:

ALTER TABLE data_links ADD CONSTRAINT unique_port_pair 
  UNIQUE (source_port_system_id, destination_port_system_id, file_system_id);

Comment on lines +54 to +64
const {session, groupId} = this.uow.getWriteContext();
const fileSystemId = dataLink.fileSystemId;
const writer = this.requireWriter();

for (const bp of boundaryPortPayloads) {
await writer.writeCreate(
{
targetTable: ENTITY_NAMES.Node,
targetSystemId: bp.nodeSystemId,
aggregateId: dataLink.systemId,
payload: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[PERFORMANCE][Medium] N+1 query pattern in boundary port creation

In TypeOrmDataLinkRepository.createDataLink(), boundary ports are created in individual loops (lines 58-75 for nodes, 77-97 for ports). Each iteration calls writer.writeCreate() which likely executes a separate database operation. For links crossing multiple subsystem boundaries, this results in N+1 queries.

Impact: Performance degradation when creating links with many boundary crossings. In a scenario with 10 subsystem boundaries, this would result in 20+ individual database writes instead of a batch operation.

Fixed Code Snippet
async createDataLink(
  dataLink: DataLink,
  boundaryPortPayloads: BoundaryPortPayload[],
  options?: EditOptions,
): Promise<void> {
  const {session, groupId} = this.uow.getWriteContext();
  const fileSystemId = dataLink.fileSystemId;
  const writer = this.requireWriter();

  // Batch node creation
  const nodeWrites = boundaryPortPayloads.map(bp => ({
    targetTable: ENTITY_NAMES.Node,
    targetSystemId: bp.nodeSystemId,
    aggregateId: dataLink.systemId,
    payload: {
      type: 'subsystem',
      parentId: bp.nodeParentId ?? null,
      fileSystemId: bp.fileSystemId,
    },
    ...options,
  }));
  
  await writer.writeBatch(nodeWrites, session.sessionId, groupId, this.manager);

  // Batch port creation
  const portWrites = boundaryPortPayloads.map(bp => ({
    targetTable: ENTITY_NAMES.DataPort,
    targetSystemId: bp.portSystemId,
    aggregateId: dataLink.systemId,
    payload: {
      dataPortId: bp.dataPortId,
      portIoType: bp.portIoType,
      isStatic: false,
      name: '',
      nodeSystemId: bp.nodeSystemId,
      fileSystemId: bp.fileSystemId,
    },
    ...options,
  }));
  
  await writer.writeBatch(portWrites, session.sessionId, groupId, this.manager);

  // ... rest of the method
}

Note: This assumes PendingChangeWriter has or can be extended with a writeBatch() method. If not available, consider implementing it for better performance.

add core/persistence layer support to create data links.
Implement POST /data-links and /data-links/with-subsystems

Signed-off-by: aboppay <aboppay@qti.qualcomm.com>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files Not Fully Analyzed

  • docs/data-links/design/data-links-post-design.md - File extension .md not supported
  • docs/data-links/requirements/data-links-post-requirements.md - File extension .md not supported
  • packages/api/src/infrastructure-wrapper/filters/all-exceptions.filter.ts - Full file content skipped due to token limit
  • packages/api/src/infrastructure-wrapper/persistence/unit-of-work/typeorm-unit-of-work.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/dto/component-collection-with-subsystems.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/dto/subsystem-components-response.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/common/swagger-doc/dto-examples/usecase-api-examples.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/data-link.controller.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-request.dto.ts - Full file content skipped due to token limit
  • packages/api/src/presentation/rest/modules/data-link/dto/request/create-data-link-with-subsystems-request.dto.ts - Full file content skipped due to token limit
  • packages/core/src/application/file-operations/upload-file/services/entity-builders/subsystem-builder.ts - Full file content skipped due to token limit
  • packages/core/src/application/orchestration/cqrs/registries/command-handler-registry.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/subsystem/subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/subsystem-data-link-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/query-services/usecase/query-models/usecase-components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/ports/persistence/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/build-traversal-entities.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link-with-subsystems.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.command.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/data-links/create/create-data-link.handler.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/dto/component-collection-dto.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/build-subsystem-tree.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/components-with-subsystems-read-model.ts - Full file content skipped due to token limit
  • packages/core/src/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.ts - Full file content skipped due to token limit
  • packages/core/src/domain/services/subsystem-data-links/subsystem-data-link-derivation.service.ts - Full file content skipped due to token limit
  • packages/core/src/index.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/conflict.exception.ts - Full file content skipped due to token limit
  • packages/core/src/shared/exceptions/index.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/file-operations/upload-file/services/entity-builders/subsystem-builder.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/data-links/create/create-data-link.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/usecase/get-component-with-subsystem/build-subsystem-tree.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/application/usecase-designer/usecase/get-component-with-subsystem/get-components-with-subsystems.handler.spec.ts - Full file content skipped due to token limit
  • packages/core/tests/unit/domain/services/subsystem-links/subsystem-data-link-derivation.service.spec.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/subsystem/db-subsystem-query-service.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/queries/usecase/usecase-query-mappers.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/data-link/data-link.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/module/module.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subgraph/subgraph.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/src/persistence-typeorm-sqllite/repositories/subsystem/subsystem.repository.ts - Full file content skipped due to token limit
  • packages/infrastructure/persistence/tests/integration/repositories/data-link/data-link-edit.repository.integration.spec.ts - Full file content skipped due to token limit

Qualcomm AI Review

Comment on lines 143 to +161
deleted: changed.deleted.map(row => baseToDataLink(row)),
};
}

async createDataLink(
dataLink: DataLink,
boundaryPortPayloads: BoundaryPortPayload[],
options?: EditOptions,
): Promise<void> {
const {session, groupId} = this.uow.getWriteContext();
const fileSystemId = dataLink.fileSystemId;
const writer = this.requireWriter();

for (const bp of boundaryPortPayloads) {
await writer.writeCreate(
{
targetTable: ENTITY_NAMES.Node,
targetSystemId: bp.nodeSystemId,
aggregateId: dataLink.systemId,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY] Missing Transaction Rollback in Repository Write Operations - High Severity

The createDataLink method in TypeOrmDataLinkRepository performs multiple sequential write operations (boundary ports, data link, SLS segments) but doesn't wrap them in an explicit transaction or handle partial failure scenarios. If any write operation fails after the first one succeeds, the database could be left in an inconsistent state with orphaned records.

The method relies on the caller to manage transactions, but there's no guarantee that the UnitOfWork has started a transaction before this method is called. Additionally, if an exception occurs during the loop iterations, previously written records won't be rolled back.

Fixed Code Snippet
async createDataLink(
  dataLink: DataLink,
  boundaryPortPayloads: BoundaryPortPayload[],
  options?: EditOptions,
): Promise<void> {
  const {session, groupId} = this.uow.getWriteContext();
  const fileSystemId = dataLink.fileSystemId;
  const writer = this.requireWriter();

  // Ensure we're in a transaction
  if (!this.uow.isInTransaction()) {
    throw new Error('createDataLink must be called within an active transaction');
  }

  try {
    // Write boundary port nodes
    for (const bp of boundaryPortPayloads) {
      await writer.writeCreate(
        {
          targetTable: ENTITY_NAMES.Node,
          targetSystemId: bp.nodeSystemId,
          aggregateId: dataLink.systemId,
          payload: {
            type: 'subsystem',
            parentId: bp.nodeParentId ?? null,
            fileSystemId: bp.fileSystemId,
          },
          ...options,
        },
        session.sessionId,
        groupId,
        this.manager,
      );
    }

    // Write boundary port data
    for (const bp of boundaryPortPayloads) {
      await writer.writeCreate(
        {
          targetTable: ENTITY_NAMES.DataPort,
          targetSystemId: bp.portSystemId,
          aggregateId: dataLink.systemId,
          payload: {
            dataPortId: bp.dataPortId,
            portIoType: bp.portIoType,
            isStatic: false,
            name: '',
            nodeSystemId: bp.nodeSystemId,
            fileSystemId: bp.fileSystemId,
          },
          ...options,
        },
        session.sessionId,
        groupId,
        this.manager,
      );
    }

    // Write data link
    await writer.writeCreate(
      {
        targetTable: ENTITY_NAMES.DataLink,
        targetSystemId: dataLink.systemId,
        aggregateId: dataLink.systemId,
        payload: {
          sourceNodeSystemId: dataLink.sourceNodeSystemId,
          destinationNodeSystemId: dataLink.destinationNodeSystemId,
          sourcePortSystemId: dataLink.sourcePortSystemId,
          destinationPortSystemId: dataLink.destinationPortSystemId,
          linkType: dataLink.linkType,
          sourceSubgraphSystemId: dataLink.sourceSubgraphSystemId,
          destSubgraphSystemId: dataLink.destSubgraphSystemId,
          isEc: dataLink.isEc ?? null,
          fileSystemId,
        },
        ...options,
      },
      session.sessionId,
      groupId,
      this.manager,
    );

    // Write SLS segments
    for (const sls of dataLink.subsystemDataLinks) {
      await writer.writeCreate(
        {
          targetTable: ENTITY_NAMES.SubsystemDataLink,
          targetSystemId: sls.systemId,
          aggregateId: dataLink.systemId,
          payload: {
            sourceNodeSystemId: sls.sourceNodeSystemId,
            destinationNodeSystemId: sls.destinationNodeSystemId,
            sourcePortSystemId: sls.sourcePortSystemId,
            destinationPortSystemId: sls.destinationPortSystemId,
            dataLinkSystemId: sls.dataLinkSystemId,
            fileSystemId,
          },
          ...options,
        },
        session.sessionId,
        groupId,
        this.manager,
      );
    }
  } catch (error) {
    // Transaction will be rolled back by the caller (UnitOfWork)
    throw error;
  }
}

Comment on lines +116 to +130
fileSystemId,
uow,
subsystemRepo,
);
} catch (error) {
if (uow.isInTransaction()) await uow.rollback();
throw error;
}
}

private async handleBranchA(
command: CreateDataLinkWithSubsystemsCommand,
srcNodeId: number,
dstNodeId: number,
srcPortId: number,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[FUNCTIONALITY] Incomplete Error Context in Handler Exception Handling - High Severity

The CreateDataLinkWithSubsystemsHandler catches all errors in a generic catch block and re-throws them without adding context about which operation failed. This makes debugging difficult when errors occur during the multi-step process (module lookup, port validation, segment derivation, persistence).

Additionally, the handler doesn't distinguish between expected domain exceptions (like validation failures) and unexpected infrastructure errors (like database connection issues), which could lead to inappropriate error responses being sent to the client.

Fixed Code Snippet
async handle(
  command: CreateDataLinkWithSubsystemsCommand,
): Promise<ComponentCollectionWithSubsystemsDto> {
  const uow = this.uow;
  await uow.startTransaction();
  try {
    const {session} = uow.getWriteContext();
    const fileSystemId = session.fileSystemId;

    const srcNodeId = Number.parseInt(command.sourceNodeSystemId, 10);
    const dstNodeId = Number.parseInt(command.destinationNodeSystemId, 10);
    const srcPortId = Number.parseInt(command.sourcePortSystemId, 10);
    const dstPortId = Number.parseInt(command.destinationPortSystemId, 10);

    // FR-DLS-04: self-loop check
    if (srcNodeId === dstNodeId) {
      throw new DomainRuleViolationException([
        {
          code: 'SELF_LOOP',
          message: `Source and destination node must differ: ${BinaryUtils.toHexString(srcNodeId)}`,
          severity: IssueSeverity.Error,
        },
      ]);
    }

    const subsystemRepo = uow.getSubsystemRepository();
    const srcIsSubsystem = await subsystemRepo.subsystemExists(
      srcNodeId,
      fileSystemId,
    );
    const dstIsSubsystem = await subsystemRepo.subsystemExists(
      dstNodeId,
      fileSystemId,
    );

    if (srcIsSubsystem || dstIsSubsystem) {
      return this.handleBranchB(
        command,
        srcNodeId,
        dstNodeId,
        srcPortId,
        dstPortId,
        srcIsSubsystem,
        dstIsSubsystem,
        fileSystemId,
        uow,
        subsystemRepo,
      );
    }

    return this.handleBranchA(
      command,
      srcNodeId,
      dstNodeId,
      srcPortId,
      dstPortId,
      fileSystemId,
      uow,
      subsystemRepo,
    );
  } catch (error) {
    if (uow.isInTransaction()) await uow.rollback();
    
    // Re-throw domain exceptions as-is
    if (
      error instanceof DomainRuleViolationException ||
      error instanceof ConflictException ||
      error instanceof ResourceNotFoundException
    ) {
      throw error;
    }
    
    // Wrap infrastructure errors with context
    throw new Error(
      `Failed to create data link with subsystems: ${error instanceof Error ? error.message : String(error)}`,
      {cause: error}
    );
  }
}

Comment on lines +53 to +98
}
return map;
}

async getPortIoType(
portSystemId: number,
fileSystemId: number,
): Promise<PortIoType | null> {
const sessionId = this.uow.getWriteContext().session.sessionId;

// Check session overlay first — a staged CREATE wins over the base table
const actions = await this.editActionsQs.getByTable(
sessionId,
ENTITY_NAMES.DataPort,
);
for (const action of actions) {
if (
action.operation === CHANGE_OPERATION.Create &&
action.targetSystemId === portSystemId
) {
const p = action.newValue as Record<string, unknown>;
if (Number(p['fileSystemId']) === fileSystemId) {
return (p['portIoType'] as PortIoType) ?? null;
}
}
}

// Fall through to base table
const row = await this.manager
.createQueryBuilder()
.select(['dp.portIoType'])
.from(ENTITY_NAMES.DataPort, 'dp')
.where('dp.systemId = :systemId AND dp.fileSystemId = :fileSystemId', {
systemId: portSystemId,
fileSystemId,
})
.getRawOne<{dp_port_io_type: string}>();

return (row?.dp_port_io_type as PortIoType) ?? null;
}

async isPortOccupiedAsSource(
portSystemId: number,
fileSystemId: number,
): Promise<boolean> {
const count = await this.manager

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[PERFORMANCE] Inefficient Sequential Port Type Lookup - Medium Severity

The getPortIoType method in TypeOrmSubsystemRepository first queries the edit_actions table by fetching ALL actions for the DataPort table in the session, then iterates through them to find a match. This is inefficient when there are many staged port changes in the session.

A more efficient approach would be to query edit_actions with a WHERE clause filtering by both targetSystemId and targetTable, or to use a single query that checks both the overlay and base table with a UNION.

Fixed Code Snippet
async getPortIoType(
  portSystemId: number,
  fileSystemId: number,
): Promise<PortIoType | null> {
  const sessionId = this.uow.getWriteContext().session.sessionId;

  // Check session overlay with targeted query
  const overlayRow = await this.manager
    .createQueryBuilder()
    .select(['ea.new_value'])
    .from('edit_actions', 'ea')
    .where(
      'ea.session_id = :sessionId AND ea.target_table = :table AND ea.target_system_id = :systemId AND ea.operation = :op AND ea.valid_until IS NULL',
      {
        sessionId,
        table: ENTITY_NAMES.DataPort,
        systemId: portSystemId,
        op: CHANGE_OPERATION.Create,
      },
    )
    .getRawOne<{ea_new_value: string}>();

  if (overlayRow) {
    const payload = JSON.parse(overlayRow.ea_new_value) as Record<string, unknown>;
    if (Number(payload['fileSystemId']) === fileSystemId) {
      return (payload['portIoType'] as PortIoType) ?? null;
    }
  }

  // Fall through to base table
  const row = await this.manager
    .createQueryBuilder()
    .select(['dp.portIoType'])
    .from(ENTITY_NAMES.DataPort, 'dp')
    .where('dp.systemId = :systemId AND dp.fileSystemId = :fileSystemId', {
      systemId: portSystemId,
      fileSystemId,
    })
    .getRawOne<{dp_port_io_type: string}>();

  return (row?.dp_port_io_type as PortIoType) ?? null;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants