From 76e5e93a53ca8899e0b88c2e17a7e67e22ce1c19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:07:26 +0000 Subject: [PATCH 1/2] Initial plan From fac0028af2b097da06b969e6ec29398f3e6c8e61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:21:51 +0000 Subject: [PATCH 2/2] Add 14 new generator types to reach 61 total, closing the 60+ website claim gap New generators: TITLE, JOB_TITLE, NATIONALITY, COMPANY_NAME, DEPARTMENT, CURRENCY_CODE, DOMAIN_NAME, USER_AGENT, LATITUDE, LONGITUDE, TIME_ZONE, BOOLEAN, LOREM, TIMESTAMP Updated: backend enum, GeneratorService logic, frontend enum, tests, README, website guide, and user guide documentation. Agent-Logs-Url: https://github.com/MaximumTrainer/OpenDataMask/sessions/93036016-950b-41e8-8c56-906e6a23008c Co-authored-by: MaximumTrainer <1376575+MaximumTrainer@users.noreply.github.com> --- README.md | 2 +- .../application/service/GeneratorService.kt | 14 +++ .../domain/model/ColumnGenerator.kt | 12 +++ .../service/GeneratorServiceTest.kt | 98 +++++++++++++++++++ .../domain/model/EnumAlignmentTest.kt | 6 ++ docs/user-guide.md | 4 + docs/website/guide.html | 10 +- frontend/src/types/index.ts | 20 ++++ .../views/__tests__/ConnectionsView.test.ts | 4 +- 9 files changed, 163 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f9e8d4e..94fb95b 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ OpenDataMask connects to your source databases, applies configurable masking/gen - **File (CSV/JSON)** — AES-encrypted upload/download, multipart REST endpoints ### Data Masking & Generation -- **47 generator types** powered by [Datafaker](https://datafaker.net/): names, addresses, emails, phones, SSNs, credit cards, IBANs, IPs, VINs, medical codes, and more +- **61 generator types** powered by [Datafaker](https://datafaker.net/): names, addresses, emails, phones, SSNs, credit cards, IBANs, IPs, VINs, medical codes, job titles, nationalities, and more - **Composite generators**: `PARTIAL_MASK`, `FORMAT_PRESERVING`, `CONDITIONAL`, `SEQUENTIAL` - **Deterministic consistency**: HMAC-SHA256 seeding ensures same input → same output across runs; linked columns share a Faker instance - **Generator presets**: 24 built-in system presets; workspace-level custom presets; one-click apply diff --git a/backend/src/main/kotlin/com/opendatamask/application/service/GeneratorService.kt b/backend/src/main/kotlin/com/opendatamask/application/service/GeneratorService.kt index bf4a5fe..b36e387 100644 --- a/backend/src/main/kotlin/com/opendatamask/application/service/GeneratorService.kt +++ b/backend/src/main/kotlin/com/opendatamask/application/service/GeneratorService.kt @@ -104,6 +104,20 @@ class GeneratorService( GeneratorType.LICENSE_PLATE -> faker.vehicle().licensePlate() GeneratorType.ORGANIZATION -> faker.company().name() GeneratorType.ACCOUNT_NUMBER -> faker.regexify("[0-9]{10}") + GeneratorType.TITLE -> faker.name().prefix() + GeneratorType.JOB_TITLE -> faker.job().title() + GeneratorType.NATIONALITY -> faker.nation().nationality() + GeneratorType.COMPANY_NAME -> faker.company().name() + GeneratorType.DEPARTMENT -> faker.commerce().department() + GeneratorType.CURRENCY_CODE -> faker.currency().code() + GeneratorType.DOMAIN_NAME -> faker.internet().domainName() + GeneratorType.USER_AGENT -> faker.internet().userAgent().replace(Regex("\\s+"), " ") + GeneratorType.LATITUDE -> faker.address().latitude() + GeneratorType.LONGITUDE -> faker.address().longitude() + GeneratorType.TIME_ZONE -> faker.address().timeZone() + GeneratorType.BOOLEAN -> faker.bool().bool() + GeneratorType.LOREM -> faker.lorem().paragraph() + GeneratorType.TIMESTAMP -> java.sql.Timestamp(faker.date().past(365 * 10, java.util.concurrent.TimeUnit.DAYS).time) GeneratorType.PARTIAL_MASK -> { val s = originalValue?.toString() ?: return null val maskChar = (params?.get("maskChar") ?: "*").firstOrNull() ?: '*' diff --git a/backend/src/main/kotlin/com/opendatamask/domain/model/ColumnGenerator.kt b/backend/src/main/kotlin/com/opendatamask/domain/model/ColumnGenerator.kt index 5893500..b9929d6 100644 --- a/backend/src/main/kotlin/com/opendatamask/domain/model/ColumnGenerator.kt +++ b/backend/src/main/kotlin/com/opendatamask/domain/model/ColumnGenerator.kt @@ -24,6 +24,18 @@ enum class GeneratorType { VIN, LICENSE_PLATE, // Other ORGANIZATION, ACCOUNT_NUMBER, + // Personal extended + TITLE, JOB_TITLE, NATIONALITY, + // Business + COMPANY_NAME, DEPARTMENT, + // Financial extended + CURRENCY_CODE, + // Network extended + DOMAIN_NAME, USER_AGENT, + // Location extended + LATITUDE, LONGITUDE, TIME_ZONE, + // Data utilities + BOOLEAN, LOREM, TIMESTAMP, // Composite / PK generators CONDITIONAL, PARTIAL_MASK, FORMAT_PRESERVING, SEQUENTIAL, RANDOM_INT } diff --git a/backend/src/test/kotlin/com/opendatamask/application/service/GeneratorServiceTest.kt b/backend/src/test/kotlin/com/opendatamask/application/service/GeneratorServiceTest.kt index 8e31239..d13174a 100644 --- a/backend/src/test/kotlin/com/opendatamask/application/service/GeneratorServiceTest.kt +++ b/backend/src/test/kotlin/com/opendatamask/application/service/GeneratorServiceTest.kt @@ -202,4 +202,102 @@ class GeneratorServiceTest { assertNotNull(result) assertInstanceOf(java.math.BigDecimal::class.java, result) } + + @Test + fun `TITLE generates a non-blank name prefix`() { + val result = service.generateValue(GeneratorType.TITLE, null, null) + assertNotNull(result) + assertTrue((result as String).isNotBlank()) + } + + @Test + fun `JOB_TITLE generates a non-blank job title`() { + val result = service.generateValue(GeneratorType.JOB_TITLE, null, null) + assertNotNull(result) + assertTrue((result as String).isNotBlank()) + } + + @Test + fun `NATIONALITY generates a non-blank nationality`() { + val result = service.generateValue(GeneratorType.NATIONALITY, null, null) + assertNotNull(result) + assertTrue((result as String).isNotBlank()) + } + + @Test + fun `COMPANY_NAME generates a non-blank company name`() { + val result = service.generateValue(GeneratorType.COMPANY_NAME, null, null) + assertNotNull(result) + assertTrue((result as String).isNotBlank()) + } + + @Test + fun `DEPARTMENT generates a non-blank department`() { + val result = service.generateValue(GeneratorType.DEPARTMENT, null, null) + assertNotNull(result) + assertTrue((result as String).isNotBlank()) + } + + @Test + fun `CURRENCY_CODE generates a non-blank currency code`() { + val result = service.generateValue(GeneratorType.CURRENCY_CODE, null, null) + assertNotNull(result) + assertTrue((result as String).isNotBlank()) + } + + @Test + fun `DOMAIN_NAME generates a non-blank domain`() { + val result = service.generateValue(GeneratorType.DOMAIN_NAME, null, null) + assertNotNull(result) + assertTrue((result as String).isNotBlank()) + } + + @Test + fun `USER_AGENT generates a non-blank user agent`() { + val result = service.generateValue(GeneratorType.USER_AGENT, null, null) + assertNotNull(result) + assertTrue((result as String).isNotBlank()) + } + + @Test + fun `LATITUDE generates a non-blank latitude`() { + val result = service.generateValue(GeneratorType.LATITUDE, null, null) + assertNotNull(result) + assertTrue((result as String).isNotBlank()) + } + + @Test + fun `LONGITUDE generates a non-blank longitude`() { + val result = service.generateValue(GeneratorType.LONGITUDE, null, null) + assertNotNull(result) + assertTrue((result as String).isNotBlank()) + } + + @Test + fun `TIME_ZONE generates a non-blank timezone`() { + val result = service.generateValue(GeneratorType.TIME_ZONE, null, null) + assertNotNull(result) + assertTrue((result as String).isNotBlank()) + } + + @Test + fun `BOOLEAN generates a Boolean value`() { + val result = service.generateValue(GeneratorType.BOOLEAN, null, null) + assertNotNull(result) + assertInstanceOf(java.lang.Boolean::class.java, result) + } + + @Test + fun `LOREM generates a non-blank paragraph`() { + val result = service.generateValue(GeneratorType.LOREM, null, null) + assertNotNull(result) + assertTrue((result as String).isNotBlank()) + } + + @Test + fun `TIMESTAMP generates a java sql Timestamp`() { + val result = service.generateValue(GeneratorType.TIMESTAMP, null, null) + assertNotNull(result) + assertInstanceOf(java.sql.Timestamp::class.java, result) + } } diff --git a/backend/src/test/kotlin/com/opendatamask/domain/model/EnumAlignmentTest.kt b/backend/src/test/kotlin/com/opendatamask/domain/model/EnumAlignmentTest.kt index 536180a..a66cf04 100644 --- a/backend/src/test/kotlin/com/opendatamask/domain/model/EnumAlignmentTest.kt +++ b/backend/src/test/kotlin/com/opendatamask/domain/model/EnumAlignmentTest.kt @@ -34,6 +34,12 @@ class EnumAlignmentTest { "IP_ADDRESS", "IPV6_ADDRESS", "MAC_ADDRESS", "URL", "VIN", "LICENSE_PLATE", "ORGANIZATION", "ACCOUNT_NUMBER", + "TITLE", "JOB_TITLE", "NATIONALITY", + "COMPANY_NAME", "DEPARTMENT", + "CURRENCY_CODE", + "DOMAIN_NAME", "USER_AGENT", + "LATITUDE", "LONGITUDE", "TIME_ZONE", + "BOOLEAN", "LOREM", "TIMESTAMP", "CONDITIONAL", "PARTIAL_MASK", "FORMAT_PRESERVING", "SEQUENTIAL", "RANDOM_INT" ) assertEquals(expected, values, "GeneratorType values do not match canonical set") diff --git a/docs/user-guide.md b/docs/user-guide.md index 0dd71f1..e3757ad 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -159,6 +159,10 @@ OpenDataMask includes 60+ built-in generators. Key examples: | `IP_ADDRESS` | `192.168.1.42` | | `ICD_CODE` | `J45.909` | | `IBAN` | `GB29NWBK60161331926819` | +| `JOB_TITLE` | `Senior Software Engineer` | +| `NATIONALITY` | `Canadian` | +| `DOMAIN_NAME` | `example.com` | +| `TIMESTAMP` | `2024-06-15 14:30:00` | | `PARTIAL_MASK` | `J*** D***` (preserves format) | | `NULL` | `null` | | `CONSTANT` | Fixed value (configured in params) | diff --git a/docs/website/guide.html b/docs/website/guide.html index 133ff5e..1f1dcf1 100644 --- a/docs/website/guide.html +++ b/docs/website/guide.html @@ -130,12 +130,14 @@

Generator Types

OpenDataMask includes 60+ built-in generators covering personal, financial, medical, and network data:

- - + + - - + + + +
CategoryGenerators
PersonalNAME, FIRST_NAME, LAST_NAME, EMAIL, PHONE, BIRTH_DATE, GENDER
AddressADDRESS, STREET_ADDRESS, CITY, STATE, ZIP_CODE, COUNTRY, GPS_COORDINATES
PersonalNAME, FIRST_NAME, LAST_NAME, EMAIL, PHONE, BIRTH_DATE, GENDER, TITLE, JOB_TITLE, NATIONALITY
AddressADDRESS, STREET_ADDRESS, CITY, STATE, ZIP_CODE, COUNTRY, GPS_COORDINATES, LATITUDE, LONGITUDE, TIME_ZONE
IdentitySSN, PASSPORT_NUMBER, DRIVERS_LICENSE, MEDICAL_RECORD_NUMBER
FinancialCREDIT_CARD, IBAN, SWIFT_CODE, MONEY_AMOUNT, BTC_ADDRESS, ACCOUNT_NUMBER
NetworkIP_ADDRESS, IPV6_ADDRESS, MAC_ADDRESS, URL
FinancialCREDIT_CARD, IBAN, SWIFT_CODE, MONEY_AMOUNT, BTC_ADDRESS, ACCOUNT_NUMBER, CURRENCY_CODE
NetworkIP_ADDRESS, IPV6_ADDRESS, MAC_ADDRESS, URL, DOMAIN_NAME, USER_AGENT
BusinessORGANIZATION, COMPANY_NAME, DEPARTMENT
MedicalICD_CODE, HEALTH_PLAN_NUMBER
Data UtilitiesBOOLEAN, LOREM, TIMESTAMP
ControlNULL, CONSTANT, PARTIAL_MASK, FORMAT_PRESERVING, SEQUENTIAL, RANDOM_INT
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index b8fa0c8..f8901ea 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -175,6 +175,26 @@ export enum GeneratorType { // Other ORGANIZATION = 'ORGANIZATION', ACCOUNT_NUMBER = 'ACCOUNT_NUMBER', + // Personal extended + TITLE = 'TITLE', + JOB_TITLE = 'JOB_TITLE', + NATIONALITY = 'NATIONALITY', + // Business + COMPANY_NAME = 'COMPANY_NAME', + DEPARTMENT = 'DEPARTMENT', + // Financial extended + CURRENCY_CODE = 'CURRENCY_CODE', + // Network extended + DOMAIN_NAME = 'DOMAIN_NAME', + USER_AGENT = 'USER_AGENT', + // Location extended + LATITUDE = 'LATITUDE', + LONGITUDE = 'LONGITUDE', + TIME_ZONE = 'TIME_ZONE', + // Data utilities + BOOLEAN = 'BOOLEAN', + LOREM = 'LOREM', + TIMESTAMP = 'TIMESTAMP', // Composite / PK generators CONDITIONAL = 'CONDITIONAL', PARTIAL_MASK = 'PARTIAL_MASK', diff --git a/frontend/src/views/__tests__/ConnectionsView.test.ts b/frontend/src/views/__tests__/ConnectionsView.test.ts index 58f88bc..73eaf5c 100644 --- a/frontend/src/views/__tests__/ConnectionsView.test.ts +++ b/frontend/src/views/__tests__/ConnectionsView.test.ts @@ -17,8 +17,8 @@ describe('ConnectionType enum', () => { }) describe('GeneratorType enum', () => { - it('includes all 47 generator types', () => { - expect(Object.keys(GeneratorType).length).toBe(47) + it('includes all 61 generator types', () => { + expect(Object.keys(GeneratorType).length).toBe(61) expect(GeneratorType.NAME).toBe('NAME') expect(GeneratorType.EMAIL).toBe('EMAIL') expect(GeneratorType.NULL).toBe('NULL')