Skip to content

feat: import Duala and Bassa vocabulary via NTeALan API - #26

Merged
ASSONDJI merged 1 commit into
mainfrom
feature/ntealan-duala-bassa-import
Jul 9, 2026
Merged

feat: import Duala and Bassa vocabulary via NTeALan API#26
ASSONDJI merged 1 commit into
mainfrom
feature/ntealan-duala-bassa-import

Conversation

@ASSONDJI

@ASSONDJI ASSONDJI commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Adds a dedicated NTeALanClient/NTeALanImportService pair (cahier des charges §5.1) that fetches, normalizes, and imports dictionary articles from the NTeALan collaborative API into content-service's word repository.

Key design points:

  • Manual JsonNode parsing instead of strict POJO binding: the API's repeatable fields (translations.equivalent, examples.example) inconsistently serialize as either an array or a bare object depending on cardinality, an XML-to-JSON conversion artifact confirmed against real API responses for both dictionaries.
  • Idempotent: re-running an import skips words already present (case-insensitive match), safe to re-trigger.
  • Per-word failure isolation: one bad entry doesn't abort the batch.
  • Separate @qualifier'd RestTemplate beans (loadBalancedRestTemplate vs externalRestTemplate) so calls to the real NTeALan domain don't get routed through Eureka's service resolution, which only applies to this project's own registered services.
  • New POST /languages/{languageId}/import-ntealan admin endpoint, triggered manually once per language.

Tests use real captured API responses as fixtures (both the array and bare-object equivalent cases) rather than synthetic data.

Verified manually end to end: Duala (dl_fr_2018) imported 192/192 words, Bassa (bs_fr_2019) imported 727/728 words (one entry filtered for missing translation, by design), zero failures, zero duplicates on re-run.

Summary by CodeRabbit

  • New Features

    • Added the ability to import words from a NTeALan dictionary into a language.
    • Added a new import endpoint that returns counts for imported, skipped, and failed items.
  • Bug Fixes

    • Improved handling of dictionary responses with varying translation formats.
    • Prevents duplicate words from being imported into the same language.
    • Import now continues when individual entries fail, instead of stopping the whole process.

Adds a dedicated NTeALanClient/NTeALanImportService pair (cahier des
charges §5.1) that fetches, normalizes, and imports dictionary
articles from the NTeALan collaborative API into content-service's
word repository.

Key design points:
- Manual JsonNode parsing instead of strict POJO binding: the API's
  repeatable fields (translations.equivalent, examples.example)
  inconsistently serialize as either an array or a bare object
  depending on cardinality, an XML-to-JSON conversion artifact
  confirmed against real API responses for both dictionaries.
- Idempotent: re-running an import skips words already present
  (case-insensitive match), safe to re-trigger.
- Per-word failure isolation: one bad entry doesn't abort the batch.
- Separate @qualifier'd RestTemplate beans (loadBalancedRestTemplate
  vs externalRestTemplate) so calls to the real NTeALan domain don't
  get routed through Eureka's service resolution, which only applies
  to this project's own registered services.
- New POST /languages/{languageId}/import-ntealan admin endpoint,
  triggered manually once per language.

Tests use real captured API responses as fixtures (both the array
and bare-object equivalent cases) rather than synthetic data.

Verified manually end to end: Duala (dl_fr_2018) imported 192/192
words, Bassa (bs_fr_2019) imported 727/728 words (one entry filtered
for missing translation, by design), zero failures, zero duplicates
on re-run.
@ASSONDJI ASSONDJI self-assigned this Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds NTeALan dictionary import functionality to the content-service. It introduces NTeALanClient and NTeALanWordEntry to fetch and parse paginated dictionary articles, NTeALanImportService to import entries as words with idempotent skip-existing logic, a new controller endpoint, OpenAPI schema, split RestTemplate beans (load-balanced vs. external), a repository existence check, and accompanying tests/fixtures.

Changes

NTeALan Import Feature

Layer / File(s) Summary
RestTemplate bean split
.../config/RestTemplateConfig.java, .../client/RecommendationClient.java
Replaces the single load-balanced RestTemplate bean with loadBalancedRestTemplate() and externalRestTemplate(); RecommendationClient explicitly injects the load-balanced bean via constructor instead of Lombok.
NTeALanClient and word entry contract
.../client/NTeALanWordEntry.java, .../client/NTeALanClient.java, .../test/.../NTeALanClientTest.java, .../test/resources/ntealan/*
Adds NTeALanWordEntry record and NTeALanClient.fetchArticles which calls the external API, parses articles handling array/object equivalent shapes, skips invalid entries, and returns empty lists on errors; covered by tests and JSON fixtures.
Word existence check
.../repository/WordRepository.java, .../service/WordService.java
Adds existsByLanguageIdAndWordIgnoreCase repository method and WordService.wordExists helper for idempotent re-imports; updates classification Javadoc.
Import service pagination/logic
.../service/NTeALanImportService.java, .../test/.../NTeALanImportServiceTest.java
Implements importDictionary validating the language, paginating articles up to MAX_PAGES, skipping existing words, creating new ones, tolerating per-entry failures, and returning ImportResult; covered by unit tests for idempotency, partial failure, and pagination termination.
Controller endpoint and OpenAPI spec
.../controller/LanguageController.java, .../resources/openapi/content-service.yaml
Adds importFromNtealan endpoint mapping to ImportResult, new OpenAPI path/operation and ImportResult schema, plus enum formatting cleanups for CreateWordRequest/Word.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant LanguageController
  participant NTeALanImportService
  participant LanguageService
  participant NTeALanClient
  participant WordService
  LanguageController->>NTeALanImportService: importDictionary(languageId, dictionaryId)
  NTeALanImportService->>LanguageService: getEntityById(languageId)
  loop pages until empty or MAX_PAGES
    NTeALanImportService->>NTeALanClient: fetchArticles(dictionaryId, page, limit)
    NTeALanClient-->>NTeALanImportService: List<NTeALanWordEntry>
    NTeALanImportService->>WordService: wordExists(languageId, word)
    alt word not existing
      NTeALanImportService->>WordService: create(CreateWordRequest)
    else word exists
      NTeALanImportService->>NTeALanImportService: increment skipped
    end
  end
  NTeALanImportService-->>LanguageController: ImportResult(imported, skipped, failed)
Loading

Possibly related PRs

  • ASSONDJI/afrilingua#3: Extends the same content-service LanguageController/WordService/WordRepository with related import plumbing (wordExists, existsByLanguageIdAndWordIgnoreCase) and updates the same OpenAPI spec for import endpoints.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding NTeALan-based Duala and Bassa vocabulary import support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/ntealan-duala-bassa-import

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ASSONDJI
ASSONDJI merged commit 0bcaa0c into main Jul 9, 2026
0 of 6 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
services/content-service/src/test/java/cm/afrilingua/content/service/NTeALanImportServiceTest.java (1)

127-138: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the MAX_PAGES safety net.

No test verifies that the loop stops at MAX_PAGES (50) when the API never returns an empty page — an important safety behavior that is currently untested.

✅ Suggested test
`@Test`
void stopsAtMaxPagesWhenApiNeverReturnsEmptyPage() {
    NTeALanWordEntry entry = new NTeALanWordEntry("aba", "partager", "verb");

    when(nteALanClient.fetchArticles(eq("dl_fr_2018"), anyInt(), anyInt()))
            .thenReturn(List.of(entry));
    when(wordService.wordExists(eq(languageId), anyString())).thenReturn(false);
    when(wordService.create(eq(languageId), any(CreateWordRequest.class)))
            .thenReturn(mock(cm.afrilingua.content.dto.Word.class));

    NTeALanImportService.ImportResult result = importService.importDictionary(languageId, "dl_fr_2018");

    assertThat(result.imported()).isEqualTo(50);
    verify(nteALanClient, times(50)).fetchArticles(eq("dl_fr_2018"), anyInt(), anyInt());
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/content-service/src/test/java/cm/afrilingua/content/service/NTeALanImportServiceTest.java`
around lines 127 - 138, Add a test in NTeALanImportServiceTest to cover the
MAX_PAGES safety net in NTeALanImportService.importDictionary: mock
nteALanClient.fetchArticles so it always returns a non-empty page, then assert
the import stops after 50 pages and the ImportResult imported count matches that
limit. Use the existing importService, nteALanClient, and wordService setup, and
verify fetchArticles is invoked exactly 50 times to prove the loop terminates at
MAX_PAGES.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@services/content-service/src/main/java/cm/afrilingua/content/client/NTeALanClient.java`:
- Around line 39-70: The fetchArticles method in NTeALanClient is swallowing
request/parse failures by returning List.of(), which makes NTeALanImportService
treat transient errors as end-of-pagination. Update fetchArticles to let
exceptions propagate (or return a failure-aware result) instead of converting
them to an empty list, and keep the empty-list behavior only for the genuine “no
articles” case after a successful response.

In
`@services/content-service/src/main/java/cm/afrilingua/content/config/RestTemplateConfig.java`:
- Around line 22-24: The externalRestTemplate() bean currently creates a plain
RestTemplate with no network safeguards, which can let NTeALan calls hang
indefinitely; update RestTemplateConfig.externalRestTemplate to configure
connect and read timeouts on the underlying request factory before returning the
bean. Keep the fix localized to externalRestTemplate and ensure the timeouts are
appropriate for calls to external services.

In
`@services/content-service/src/main/java/cm/afrilingua/content/service/NTeALanImportService.java`:
- Around line 42-46: The import loop in NTeALanImportService#importDictionary
silently exits when the MAX_PAGES safety limit is hit, which can make a partial
import look complete. Update the logic in importDictionary to detect when the
loop ends because page reached MAX_PAGES rather than entries.isEmpty(), and emit
a warning through the existing logger in that case. Keep the current completion
log, but make sure it clearly distinguishes a full import from a truncated one
so large dictionaries are not misreported as fully imported.
- Around line 48-66: Add a database-level uniqueness guard for words because
WordService’s existence check can race with concurrent writes. Update the words
schema in the migration to enforce uniqueness per language using a
case-insensitive key on word, then adjust WordService.create and the
NTeALanImportService import flow to catch that duplicate-key path and count it
as skipped instead of failed, using the existing WordService and
NTeALanImportService symbols to locate the change.

---

Nitpick comments:
In
`@services/content-service/src/test/java/cm/afrilingua/content/service/NTeALanImportServiceTest.java`:
- Around line 127-138: Add a test in NTeALanImportServiceTest to cover the
MAX_PAGES safety net in NTeALanImportService.importDictionary: mock
nteALanClient.fetchArticles so it always returns a non-empty page, then assert
the import stops after 50 pages and the ImportResult imported count matches that
limit. Use the existing importService, nteALanClient, and wordService setup, and
verify fetchArticles is invoked exactly 50 times to prove the loop terminates at
MAX_PAGES.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ff41194f-1d8b-4f59-b808-0cecaff09f43

📥 Commits

Reviewing files that changed from the base of the PR and between 251baa1 and b43ee40.

📒 Files selected for processing (15)
  • services/content-service/src/main/java/cm/afrilingua/content/client/NTeALanClient.java
  • services/content-service/src/main/java/cm/afrilingua/content/client/NTeALanWordEntry.java
  • services/content-service/src/main/java/cm/afrilingua/content/client/RecommendationClient.java
  • services/content-service/src/main/java/cm/afrilingua/content/config/RestTemplateConfig.java
  • services/content-service/src/main/java/cm/afrilingua/content/controller/LanguageController.java
  • services/content-service/src/main/java/cm/afrilingua/content/repository/WordRepository.java
  • services/content-service/src/main/java/cm/afrilingua/content/service/NTeALanImportService.java
  • services/content-service/src/main/java/cm/afrilingua/content/service/WordService.java
  • services/content-service/src/main/resources/openapi/content-service.yaml
  • services/content-service/src/test/java/cm/afrilingua/content/client/NTeALanClientTest.java
  • services/content-service/src/test/java/cm/afrilingua/content/service/NTeALanImportServiceTest.java
  • services/content-service/src/test/resources/ntealan/bassa_single_equivalent.json
  • services/content-service/src/test/resources/ntealan/duala_array_equivalent.json
  • services/content-service/src/test/resources/ntealan/empty_page.json
  • services/content-service/src/test/resources/ntealan/missing_translation.json

Comment on lines +39 to +70
public List<NTeALanWordEntry> fetchArticles(String dictionaryId, int page, int limit) {
String url = String.format("%s/%s?limit=%d&page=%d&sort=ASC", BASE_URL, dictionaryId, limit, page);

String rawResponse;
try {
rawResponse = restTemplate.getForObject(url, String.class);
} catch (Exception ex) {
log.error("Failed to fetch NTeALan dictionary {} page {}", dictionaryId, page, ex);
return List.of();
}

if (rawResponse == null || rawResponse.isBlank()) {
return List.of();
}

try {
JsonNode root = objectMapper.readTree(rawResponse);
JsonNode articles = root.path("articles");

List<NTeALanWordEntry> entries = new ArrayList<>();
for (JsonNode articleWrapper : articles) {
NTeALanWordEntry entry = parseEntry(articleWrapper);
if (entry != null) {
entries.add(entry);
}
}
return entries;
} catch (Exception ex) {
log.error("Failed to parse NTeALan response for dictionary {} page {}", dictionaryId, page, ex);
return List.of();
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how NTeALanImportService uses the empty list returned by fetchArticles —
# specifically whether it treats empty as "stop pagination" with no retry on error.
rg -n --type=java -C5 'fetchArticles' services/content-service/src/main/java/cm/afrilingua/content/service/NTeALanImportService.java 2>/dev/null || echo "Import service not found at expected path"
fd NTeALanImportService.java services/content-service/src/main/java

Repository: ASSONDJI/afrilingua

Length of output: 650


Propagate fetch failures instead of returning an empty page. NTeALanImportService breaks pagination on entries.isEmpty(), so swallowing fetch exceptions here turns a transient network error into a silent, truncated import. Let the exception bubble up or return a result that distinguishes “no more data” from “request failed.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/content-service/src/main/java/cm/afrilingua/content/client/NTeALanClient.java`
around lines 39 - 70, The fetchArticles method in NTeALanClient is swallowing
request/parse failures by returning List.of(), which makes NTeALanImportService
treat transient errors as end-of-pagination. Update fetchArticles to let
exceptions propagate (or return a failure-aware result) instead of converting
them to an empty list, and keep the empty-list behavior only for the genuine “no
articles” case after a successful response.

Comment on lines +22 to +24
public RestTemplate externalRestTemplate() {
return new RestTemplate();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add connect and read timeouts to externalRestTemplate.

A plain new RestTemplate() uses SimpleClientHttpRequestFactory with no connect or read timeout, meaning calls to the NTeALan API can block indefinitely if the external service is slow or unresponsive. Since this bean is specifically for external domains outside your control, this is a real availability risk for the import endpoint.

⏱️ Proposed fix
 `@Bean`
 public RestTemplate externalRestTemplate() {
-    return new RestTemplate();
+    var factory = new SimpleClientHttpRequestFactory();
+    factory.setConnectTimeout(5_000);
+    factory.setReadTimeout(15_000);
+    return new RestTemplate(factory);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public RestTemplate externalRestTemplate() {
return new RestTemplate();
}
public RestTemplate externalRestTemplate() {
var factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5_000);
factory.setReadTimeout(15_000);
return new RestTemplate(factory);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/content-service/src/main/java/cm/afrilingua/content/config/RestTemplateConfig.java`
around lines 22 - 24, The externalRestTemplate() bean currently creates a plain
RestTemplate with no network safeguards, which can let NTeALan calls hang
indefinitely; update RestTemplateConfig.externalRestTemplate to configure
connect and read timeouts on the underlying request factory before returning the
bean. Keep the fix localized to externalRestTemplate and ensure the timeouts are
appropriate for calls to external services.

Comment on lines +42 to +46
for (int page = 1; page <= MAX_PAGES; page++) {
List<NTeALanWordEntry> entries = nteALanClient.fetchArticles(dictionaryId, page, PAGE_SIZE);
if (entries.isEmpty()) {
break;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Log a warning when the MAX_PAGES safety limit is reached.

If a dictionary has more than 5000 entries (50 × 100), the loop silently stops. The completion log at line 69 would show what looks like a successful full import, masking unimported entries.

🛡️ Proposed fix
     for (int page = 1; page <= MAX_PAGES; page++) {
         List<NTeALanWordEntry> entries = nteALanClient.fetchArticles(dictionaryId, page, PAGE_SIZE);
         if (entries.isEmpty()) {
             break;
         }

+        if (page == MAX_PAGES) {
+            log.warn("Reached MAX_PAGES ({}) for dictionary {} -- there may be unimported entries remaining", MAX_PAGES, dictionaryId);
+        }
+
         for (NTeALanWordEntry entry : entries) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (int page = 1; page <= MAX_PAGES; page++) {
List<NTeALanWordEntry> entries = nteALanClient.fetchArticles(dictionaryId, page, PAGE_SIZE);
if (entries.isEmpty()) {
break;
}
for (int page = 1; page <= MAX_PAGES; page++) {
List<NTeALanWordEntry> entries = nteALanClient.fetchArticles(dictionaryId, page, PAGE_SIZE);
if (entries.isEmpty()) {
break;
}
if (page == MAX_PAGES) {
log.warn("Reached MAX_PAGES ({}) for dictionary {} -- there may be unimported entries remaining", MAX_PAGES, dictionaryId);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/content-service/src/main/java/cm/afrilingua/content/service/NTeALanImportService.java`
around lines 42 - 46, The import loop in NTeALanImportService#importDictionary
silently exits when the MAX_PAGES safety limit is hit, which can make a partial
import look complete. Update the logic in importDictionary to detect when the
loop ends because page reached MAX_PAGES rather than entries.isEmpty(), and emit
a warning through the existing logger in that case. Keep the current completion
log, but make sure it clearly distinguishes a full import from a truncated one
so large dictionaries are not misreported as fully imported.

Comment on lines +48 to +66
for (NTeALanWordEntry entry : entries) {
try {
if (wordService.wordExists(languageId, entry.word())) {
skipped++;
continue;
}

CreateWordRequest request = new CreateWordRequest()
.word(entry.word())
.translation(entry.translation())
.grammaticalCategory(entry.grammaticalCategory());

wordService.create(languageId, request);
imported++;
} catch (Exception ex) {
log.warn("Failed to import NTeALan word '{}' for dictionary {}", entry.word(), dictionaryId, ex);
failed++;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for unique constraints on the Word entity
rg -n -C3 'uniqueConstraints|UniqueConstraint|`@Table`' --type java \
  -g '**/Word.java' \
  services/content-service/src/main/java/cm/afrilingua/content/entity/

# Also check for Flyway/Liquibase migration files referencing the word table
fd -e sql . services/content-service/src/main/resources/ | head -20
rg -n -i 'unique.*word|word.*unique' services/content-service/src/main/resources/ --type sql

Repository: ASSONDJI/afrilingua

Length of output: 1003


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Word entity ==\n'
cat -n services/content-service/src/main/java/cm/afrilingua/content/entity/Word.java

printf '\n== V1 migration ==\n'
cat -n services/content-service/src/main/resources/db/migration/V1__create_languages_and_words_tables.sql

printf '\n== V2 migration ==\n'
cat -n services/content-service/src/main/resources/db/migration/V2__add_tone_fields_to_words.sql

printf '\n== Search for any word-table unique indexes/constraints ==\n'
rg -n -i 'unique|index|constraint' services/content-service/src/main/resources/db/migration/ services/content-service/src/main/java/cm/afrilingua/content/entity/Word.java

Repository: ASSONDJI/afrilingua

Length of output: 3971


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== WordService ==\n'
cat -n services/content-service/src/main/java/cm/afrilingua/content/service/WordService.java

printf '\n== Word repository matches ==\n'
rg -n -C3 'wordExists|IgnoreCase|findBy.*Word|existsBy.*Word|lower\\(|LOWER\\(' services/content-service/src/main/java/cm/afrilingua/content/

printf '\n== Related repository files ==\n'
fd -e java . services/content-service/src/main/java/cm/afrilingua/content/ | rg 'Repository|Service|Word'

Repository: ASSONDJI/afrilingua

Length of output: 5675


Add a database unique constraint for words
services/content-service/src/main/java/cm/afrilingua/content/service/WordService.java checks existence case-insensitively, but words has no unique constraint in services/content-service/src/main/resources/db/migration/V1__create_languages_and_words_tables.sql. A concurrent import or create can still slip in a duplicate between the check and save(). Add a unique constraint/index on (language_id, lower(word)) and treat that violation as a skip.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/content-service/src/main/java/cm/afrilingua/content/service/NTeALanImportService.java`
around lines 48 - 66, Add a database-level uniqueness guard for words because
WordService’s existence check can race with concurrent writes. Update the words
schema in the migration to enforce uniqueness per language using a
case-insensitive key on word, then adjust WordService.create and the
NTeALanImportService import flow to catch that duplicate-key path and count it
as skipped instead of failed, using the existing WordService and
NTeALanImportService symbols to locate the change.

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.

1 participant