mongo-data-anonymizer 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintignore ADDED
@@ -0,0 +1 @@
1
+ test/
package/.eslintrc.js ADDED
@@ -0,0 +1,25 @@
1
+ module.exports = {
2
+ root: true,
3
+ parser: '@typescript-eslint/parser',
4
+ parserOptions: {
5
+ project: 'tsconfig.json',
6
+ tsconfigRootDir: __dirname,
7
+ sourceType: 'module',
8
+ },
9
+ plugins: ['@typescript-eslint', 'prettier'],
10
+ extends: [
11
+ 'eslint:recommended',
12
+ 'plugin:@typescript-eslint/recommended',
13
+ 'prettier',
14
+ ],
15
+ rules: {
16
+ 'prettier/prettier': 'error',
17
+ '@typescript-eslint/explicit-function-return-type': 'off',
18
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
19
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
20
+ },
21
+ env: {
22
+ node: true,
23
+ jest: true,
24
+ },
25
+ };
@@ -0,0 +1,37 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ unit-tests:
11
+ name: Unit tests
12
+ uses: ./.github/workflows/test.yml
13
+
14
+ release:
15
+ name: Release package
16
+ needs: [unit-tests]
17
+ runs-on: ubuntu-latest
18
+
19
+ steps:
20
+ - name: Checkout code
21
+ uses: actions/checkout@v2
22
+
23
+ - name: Set up Node.js
24
+ uses: actions/setup-node@v2
25
+ with:
26
+ node-version: '16'
27
+
28
+ - name: Install dependencies
29
+ run: npm install
30
+
31
+ - name: Build package
32
+ run: npm run build
33
+
34
+ - name: Publish to npm
35
+ run: npm publish
36
+ env:
37
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1,22 @@
1
+ name: Test
2
+
3
+ on: [push, pull_request, workflow_call]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+
9
+ steps:
10
+ - name: Checkout code
11
+ uses: actions/checkout@v2
12
+
13
+ - name: Set up Node.js
14
+ uses: actions/setup-node@v2
15
+ with:
16
+ node-version: '16'
17
+
18
+ - name: Install dependencies
19
+ run: npm install
20
+
21
+ - name: Run test
22
+ run: npm test
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ v18
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "singleQuote": true,
3
+ "trailingComma": "all",
4
+ "printWidth": 80,
5
+ "tabWidth": 2,
6
+ "semi": true
7
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 vaneavasco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # MongoDB Anonymizer
2
+
3
+ This package allows you to anonymize specified fields in MongoDB collections and copy them to a target database. It is based on the [mongodb-anonymizer](https://github.com/rap2hpoutre/mongodb-anonymizer) package by [rap2hpoutre](https://github.com/rap2hpoutre).
4
+
5
+ ## Installation
6
+
7
+ You can install the package globally using npm:
8
+
9
+ ```bash
10
+ npm install -g mongodb-anonymizer
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ To use the package, you need to provide the source and target MongoDB URIs, the fields to anonymize, and optionally, the collections to anonymize or ignore, and the batch size. Here is an example:
16
+
17
+ ```bash
18
+ mongodb-anonymizer --sourceUri "mongodb://localhost:27017/source" --targetUri "mongodb://localhost:27017/target" --fieldList "email,password" --collectionList "users,admins" --ignoreCollections "logs" --batchSize 500
19
+ ```
20
+
21
+ In this example, the `email` and `password` fields in the `users` and `admins` collections will be anonymized, the `logs` collection will be ignored, and the batch size for processing documents is set to `500`.
22
+
23
+ ## Options
24
+
25
+ Here are the available options:
26
+
27
+ - `--sourceUri`: The MongoDB URI of the source database.
28
+ - `--targetUri`: The MongoDB URI of the target database.
29
+ - `--fieldList`: A comma-separated list of fields to anonymize. You can use the `+` or `-` modifiers to add or remove fields from the default list respectively. For example, `+age` will add `age` to the default fields, and `-email` will remove `email` from the default fields.
30
+ - `--collectionList`: (Optional) A comma-separated list of collections to anonymize. If not provided, all collections will be anonymized.
31
+ - `--ignoreCollections`: (Optional) A comma-separated list of collections to ignore during the anonymization process.
32
+ - `--batchSize`: (Optional) The number of documents to process at a time. Defaults to `1000`.
33
+ - `--copyNonAnonymized`: (Optional) If set, non-anonymized collections will be copied as-is to the target database. By default, non-anonymized collections are not copied.
34
+
35
+ ## Note
36
+
37
+ This package does not delete any data from the source database. It creates a copy of the data in the target database with the specified fields anonymized. Please ensure that you have the necessary permissions and storage capacity before using this package.
@@ -0,0 +1,9 @@
1
+ export declare class Anonymize {
2
+ anonymizeBatch(batch: any[], collectionName: string, list: string[]): any[];
3
+ private getKeysToAnonymize;
4
+ private anonymizeDocument;
5
+ private anonymizeValue;
6
+ private applyReplacement;
7
+ private getFakerValue;
8
+ private getFakerValueForField;
9
+ }
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Anonymize = void 0;
4
+ const faker_1 = require("@faker-js/faker");
5
+ class Anonymize {
6
+ anonymizeBatch(batch, collectionName, list) {
7
+ const keysToAnonymize = this.getKeysToAnonymize(list, collectionName);
8
+ const fieldsToAnonymize = keysToAnonymize.map((item) => item.field);
9
+ return batch.map((document) => this.anonymizeDocument(document, fieldsToAnonymize, keysToAnonymize));
10
+ }
11
+ getKeysToAnonymize(list, collectionName) {
12
+ return list
13
+ .filter((item) => !item.match(/^[a-z_]+\./gi) || item.startsWith(`${collectionName}.`))
14
+ .map((item) => ({
15
+ field: item.replace(`${collectionName}.`, "").replace(/:(?:.*)$/, "").toLowerCase(),
16
+ replacement: item.includes(":") ? item.replace(/^(?:.*):/, "") : null,
17
+ }));
18
+ }
19
+ anonymizeDocument(document, fieldsToAnonymize, keysToAnonymize) {
20
+ var _a;
21
+ const anonymizedDocument = {};
22
+ for (const key in document) {
23
+ if (!document.hasOwnProperty(key))
24
+ continue;
25
+ if (fieldsToAnonymize.includes(key.toLowerCase())) {
26
+ if (typeof document[key] === 'object') {
27
+ if (Array.isArray(document[key]) && document[key].length > 0) {
28
+ anonymizedDocument[key] = document[key].map((item) => this.anonymizeDocument(item, fieldsToAnonymize, keysToAnonymize));
29
+ continue;
30
+ }
31
+ anonymizedDocument[key] = this.anonymizeDocument(document[key], fieldsToAnonymize, keysToAnonymize);
32
+ continue;
33
+ }
34
+ anonymizedDocument[key] = this.anonymizeValue(key.toLowerCase(), (_a = keysToAnonymize.find((item) => item.field === key.toLowerCase())) === null || _a === void 0 ? void 0 : _a.replacement);
35
+ }
36
+ else {
37
+ anonymizedDocument[key] = document[key];
38
+ }
39
+ }
40
+ return anonymizedDocument;
41
+ }
42
+ anonymizeValue(key, replacement) {
43
+ if (replacement) {
44
+ return this.applyReplacement(replacement);
45
+ }
46
+ return this.getFakerValueForField(key);
47
+ }
48
+ applyReplacement(replacement) {
49
+ if (replacement.startsWith("faker")) {
50
+ return this.getFakerValue(replacement);
51
+ }
52
+ switch (replacement) {
53
+ case "[]": return [];
54
+ case "{}": return {};
55
+ case "null": return null;
56
+ default: {
57
+ if (replacement.startsWith("[") || replacement.startsWith("{")) {
58
+ try {
59
+ return JSON.parse(decodeURIComponent(replacement));
60
+ }
61
+ catch (error) {
62
+ throw new Error(`Failed to parse replacement JSON: ${error === null || error === void 0 ? void 0 : error.message}`);
63
+ }
64
+ }
65
+ return replacement;
66
+ }
67
+ }
68
+ }
69
+ getFakerValue(replacement) {
70
+ const parts = replacement.split(".");
71
+ if (parts.length !== 3) {
72
+ throw new Error(`Invalid format for replacement: ${replacement}. Expected format 'faker.category.method'`);
73
+ }
74
+ const [, category, method] = parts;
75
+ const fakerCategory = faker_1.faker[category];
76
+ if (!fakerCategory) {
77
+ throw new Error(`Invalid faker category: ${category}`);
78
+ }
79
+ const fakerMethod = fakerCategory[method];
80
+ if (typeof fakerMethod !== 'function') {
81
+ throw new Error(`Invalid faker method: ${method} in category ${category}`);
82
+ }
83
+ return fakerMethod();
84
+ }
85
+ getFakerValueForField(key) {
86
+ if (key.includes("email"))
87
+ return faker_1.faker.internet.email().toLowerCase();
88
+ if (key.includes("firstname"))
89
+ return faker_1.faker.person.firstName();
90
+ if (key.includes("lastname"))
91
+ return faker_1.faker.person.lastName();
92
+ if (key === "description")
93
+ return faker_1.faker.lorem.sentence();
94
+ if (key.endsWith("address"))
95
+ return faker_1.faker.location.streetAddress();
96
+ if (key.endsWith("city"))
97
+ return faker_1.faker.location.city();
98
+ if (key.endsWith("country"))
99
+ return faker_1.faker.location.country();
100
+ if (key.endsWith("phone"))
101
+ return faker_1.faker.phone.number();
102
+ if (key.endsWith("comment"))
103
+ return faker_1.faker.lorem.sentence();
104
+ if (key.endsWith("date"))
105
+ return faker_1.faker.date.past();
106
+ if (key.endsWith("name"))
107
+ return faker_1.faker.person.fullName();
108
+ return faker_1.faker.word.sample();
109
+ }
110
+ }
111
+ exports.Anonymize = Anonymize;
112
+ //# sourceMappingURL=anonymize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anonymize.js","sourceRoot":"","sources":["../../src/anonymization/anonymize.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export declare class Database {
2
+ private uri;
3
+ private name;
4
+ private client;
5
+ private db;
6
+ constructor(uri: string, name: string);
7
+ connect(): Promise<void>;
8
+ close(): Promise<void>;
9
+ getCollections(): Promise<string[]>;
10
+ getCursor(collectionName: string): import("mongodb").FindCursor<import("mongodb").WithId<import("bson").Document>> | null;
11
+ insertAnonymizedBatch(collectionName: string, anonymizedBatch: any[]): Promise<void>;
12
+ dropCollectionIfExists(collectionName: string): Promise<void>;
13
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.Database = void 0;
13
+ const mongodb_1 = require("mongodb");
14
+ const bunyan = require("bunyan");
15
+ const log = bunyan.createLogger({ name: "DatabaseHandler" });
16
+ class Database {
17
+ constructor(uri, name) {
18
+ this.uri = uri;
19
+ this.name = name;
20
+ this.client = null;
21
+ this.db = null;
22
+ }
23
+ connect() {
24
+ return __awaiter(this, void 0, void 0, function* () {
25
+ try {
26
+ log.info(`Connecting to ${this.name} database...`);
27
+ this.client = new mongodb_1.MongoClient(this.uri);
28
+ yield this.client.connect();
29
+ this.db = this.client.db();
30
+ log.info(`Successfully connected to ${this.name} database`);
31
+ }
32
+ catch (error) {
33
+ throw new Error(`Failed to connect to ${this.name} database: ${error.message}`);
34
+ }
35
+ });
36
+ }
37
+ close() {
38
+ var _a;
39
+ return __awaiter(this, void 0, void 0, function* () {
40
+ yield ((_a = this === null || this === void 0 ? void 0 : this.client) === null || _a === void 0 ? void 0 : _a.close());
41
+ log.info(`Closed database connection`);
42
+ });
43
+ }
44
+ getCollections() {
45
+ var _a;
46
+ return __awaiter(this, void 0, void 0, function* () {
47
+ const collections = yield ((_a = this === null || this === void 0 ? void 0 : this.db) === null || _a === void 0 ? void 0 : _a.listCollections().toArray());
48
+ return collections ? collections.map((item) => item.name) : [];
49
+ });
50
+ }
51
+ getCursor(collectionName) {
52
+ return this.db ? this.db.collection(collectionName).find() : null;
53
+ }
54
+ insertAnonymizedBatch(collectionName, anonymizedBatch) {
55
+ return __awaiter(this, void 0, void 0, function* () {
56
+ if (this.db) {
57
+ yield this.db.collection(collectionName).insertMany(anonymizedBatch);
58
+ }
59
+ });
60
+ }
61
+ dropCollectionIfExists(collectionName) {
62
+ return __awaiter(this, void 0, void 0, function* () {
63
+ if (this.db && (yield this.db.collection(collectionName).countDocuments()) > 0) {
64
+ yield this.db.collection(collectionName).drop();
65
+ log.info(`Dropped existing collection: ${collectionName}`);
66
+ }
67
+ });
68
+ }
69
+ }
70
+ exports.Database = Database;
71
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/anonymization/database.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qCAA0C;AAC1C,iCAAiC;AAEjC,MAAM,GAAG,GAAG,MAAM,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC;AAE7D,MAAa,QAAQ;IAIjB,YAAoB,GAAW,EAAU,IAAY;QAAjC,QAAG,GAAH,GAAG,CAAQ;QAAU,SAAI,GAAJ,IAAI,CAAQ;QAH7C,WAAM,GAAuB,IAAI,CAAC;QAClC,OAAE,GAAc,IAAI,CAAC;IAE2B,CAAC;IAEnD,OAAO;;YACT,IAAI;gBACA,GAAG,CAAC,IAAI,CAAC,iBAAiB,IAAI,CAAC,IAAI,cAAc,CAAC,CAAC;gBACnD,IAAI,CAAC,MAAM,GAAG,IAAI,qBAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACxC,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC5B,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;gBAC3B,GAAG,CAAC,IAAI,CAAC,6BAA6B,IAAI,CAAC,IAAI,WAAW,CAAC,CAAC;aAC/D;YAAC,OAAO,KAAK,EAAE;gBACZ,MAAM,IAAI,KAAK,CAAC,wBAAwB,IAAI,CAAC,IAAI,cAAe,KAAe,CAAC,OAAO,EAAE,CAAC,CAAC;aAC9F;QACL,CAAC;KAAA;IAEK,KAAK;;;YACP,MAAM,CAAA,MAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,MAAM,0CAAE,KAAK,EAAE,CAAA,CAAC;YAC5B,GAAG,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;;KAC1C;IAEK,cAAc;;;YAChB,MAAM,WAAW,GAAG,MAAM,CAAA,MAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,EAAE,0CAAE,eAAe,GAAG,OAAO,EAAE,CAAA,CAAC;YAChE,OAAO,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;;KAClE;IAED,SAAS,CAAC,cAAsB;QAC5B,OAAO,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,CAAC;IAEK,qBAAqB,CAAC,cAAsB,EAAE,eAAsB;;YACtE,IAAI,IAAI,CAAC,EAAE,EAAE;gBACT,MAAM,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC;aACxE;QACL,CAAC;KAAA;IAEK,sBAAsB,CAAC,cAAsB;;YAC/C,IAAI,IAAI,CAAC,EAAE,IAAI,CAAA,MAAM,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,cAAc,EAAE,IAAG,CAAC,EAAE;gBAC1E,MAAM,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,CAAC;gBAChD,GAAG,CAAC,IAAI,CAAC,gCAAgC,cAAc,EAAE,CAAC,CAAC;aAC9D;QACL,CAAC;KAAA;CACJ;AA5CD,4BA4CC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const main_1 = require("./main");
5
+ (0, main_1.main)().catch((error) => { console.log(error.message); });
6
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;AACA,iCAA8B;AAE9B,IAAA,WAAI,GAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,GAAE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA,CAAA,CAAC,CAAC,CAAC"}
package/dist/main.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function main(): Promise<void>;
package/dist/main.js ADDED
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.main = void 0;
13
+ const database_1 = require("./anonymization/database");
14
+ const anonymize_1 = require("./anonymization/anonymize");
15
+ const field_utils_1 = require("./utils/field-utils");
16
+ const log = require('bunyan').createLogger({ name: "Main" });
17
+ const defaultFields = ['email', 'name', 'description', 'address', 'city', 'country', 'phone', 'comment', 'birthdate', 'firstname', 'lastname', 'fullname'];
18
+ function main() {
19
+ return __awaiter(this, void 0, void 0, function* () {
20
+ const config = parseArgs(process.argv);
21
+ const sourceDb = new database_1.Database(config.sourceUri, 'source');
22
+ const targetDb = new database_1.Database(config.targetUri, 'target');
23
+ const anonymizer = new anonymize_1.Anonymize();
24
+ try {
25
+ yield sourceDb.connect();
26
+ yield targetDb.connect();
27
+ const collections = yield sourceDb.getCollections();
28
+ for (const collectionName of collections) {
29
+ const copyNonAnonymized = config.copyNonAnonymized && (!config.collectionList.includes(collectionName) || config.ignoreCollections.includes(collectionName));
30
+ if (config.ignoreCollections.includes(collectionName) && !config.copyNonAnonymized) {
31
+ log.info(`Skipping collection anonymization for ${collectionName}. Collection is in ignore list.`);
32
+ continue;
33
+ }
34
+ if (config.collectionList.length > 0 && !config.collectionList.includes(collectionName) && !config.copyNonAnonymized) {
35
+ log.info(`Skipping collection anonymization for ${collectionName}. Collection is not in the collection list.`);
36
+ continue;
37
+ }
38
+ if (!copyNonAnonymized) {
39
+ log.info(`Anonymizing collection ${collectionName}.`);
40
+ }
41
+ else {
42
+ log.info(`Copying collection ${collectionName} without anonymization.`);
43
+ }
44
+ const cursor = sourceDb.getCursor(collectionName);
45
+ while (yield (cursor === null || cursor === void 0 ? void 0 : cursor.hasNext())) {
46
+ const batch = yield getBatch(cursor, config.batchSize);
47
+ const anonymizedBatch = !copyNonAnonymized ? anonymizer.anonymizeBatch(batch, collectionName, config.fieldList) : batch;
48
+ yield targetDb.insertAnonymizedBatch(collectionName, anonymizedBatch);
49
+ }
50
+ }
51
+ log.info("Anonymization process completed successfully!");
52
+ }
53
+ catch (error) {
54
+ log.error(`An error occurred during the anonymization process: ${error.message}`);
55
+ }
56
+ finally {
57
+ yield sourceDb.close();
58
+ yield targetDb.close();
59
+ }
60
+ });
61
+ }
62
+ exports.main = main;
63
+ function parseArgs(argv) {
64
+ var _a, _b;
65
+ const yargs = require('yargs/yargs');
66
+ const { hideBin } = require('yargs/helpers');
67
+ const args = yargs(hideBin(argv))
68
+ .option('sourceUri', { type: 'string', demandOption: true })
69
+ .option('targetUri', { type: 'string', demandOption: true })
70
+ .option('fieldList', { type: 'string', demandOption: true, default: defaultFields.join(',') })
71
+ .option('collectionList', { type: 'string' })
72
+ .option('ignoreCollections', { type: 'string' })
73
+ .option('batchSize', { type: 'number', default: 1000 })
74
+ .option('copyNonAnonymized', { type: 'boolean', default: false })
75
+ .argv;
76
+ return {
77
+ sourceUri: args.sourceUri,
78
+ targetUri: args.targetUri,
79
+ fieldList: (0, field_utils_1.getFieldList)(args.fieldList, defaultFields),
80
+ ignoreCollections: ((_a = args.ignoreCollections) === null || _a === void 0 ? void 0 : _a.split(',')) || [],
81
+ collectionList: ((_b = args.collectionList) === null || _b === void 0 ? void 0 : _b.split(',')) || [],
82
+ batchSize: args.batchSize,
83
+ copyNonAnonymized: args.copyNonAnonymized
84
+ };
85
+ }
86
+ function getBatch(cursor, batchSize) {
87
+ return __awaiter(this, void 0, void 0, function* () {
88
+ const batch = [];
89
+ for (let i = 0; i < batchSize && (yield cursor.hasNext()); i++) {
90
+ batch.push(yield cursor.next());
91
+ }
92
+ return batch;
93
+ });
94
+ }
95
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare function getFieldList(fieldListOption: string, defaultFields?: string[]): string[];
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getFieldList = void 0;
4
+ function getFieldList(fieldListOption, defaultFields = []) {
5
+ if (fieldListOption.length) {
6
+ if (fieldListOption.startsWith('+')) {
7
+ return defaultFields.concat(fieldListOption.slice(1).split(','));
8
+ }
9
+ if (fieldListOption.startsWith('-')) {
10
+ const fieldsToRemove = fieldListOption.slice(1).split(',');
11
+ return defaultFields.filter(field => !fieldsToRemove.includes(field));
12
+ }
13
+ return fieldListOption.split(',');
14
+ }
15
+ return [...defaultFields];
16
+ }
17
+ exports.getFieldList = getFieldList;
18
+ //# sourceMappingURL=field-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"field-utils.js","sourceRoot":"","sources":["../../src/utils/field-utils.ts"],"names":[],"mappings":";;;AAAA,SAAgB,YAAY,CAAC,eAAuB,EAAE,gBAA0B,EAAE;IAC9E,IAAI,eAAe,CAAC,MAAM,EAAE;QACxB,IAAI,eAAe,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;YACjC,OAAO,aAAa,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;SACpE;QAED,IAAI,eAAe,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE;YACjC,MAAM,cAAc,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC3D,OAAO,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,cAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;SACzE;QAED,OAAO,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;KACrC;IAED,OAAO,CAAC,GAAG,aAAa,CAAC,CAAC;AAC9B,CAAC;AAfD,oCAeC"}
package/jest.config.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/test'],
5
+ transform: {
6
+ '^.+\\.ts?$': 'ts-jest',
7
+ },
8
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "mongo-data-anonymizer",
3
+ "version": "0.2.0",
4
+ "description": "Anonymize specified fields in MongoDB collections and copy them to a target database.",
5
+ "main": "dist/main.js",
6
+ "bin": {
7
+ "mongo-data-anonymizer": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "ts-node src/main.ts",
11
+ "build": "tsc",
12
+ "test": "jest",
13
+ "lint": "eslint 'src/**/*.{js,ts}'",
14
+ "format": "prettier --write 'src/**/*.{js,ts}'",
15
+ "run": "node dist/cli.js",
16
+ "run:dev": "ts-node src/cli.ts",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "dependencies": {
20
+ "bunyan": "^1.8.15",
21
+ "faker": "^5.0.0",
22
+ "mongodb": "^4.0.0",
23
+ "yargs": "^17.7.2"
24
+ },
25
+ "devDependencies": {
26
+ "@faker-js/faker": "^8.4.1",
27
+ "@types/bunyan": "^1.8.11",
28
+ "@types/faker": "^6.6.9",
29
+ "@types/jest": "^26.0.0",
30
+ "@types/node": "^16.0.0",
31
+ "@typescript-eslint/eslint-plugin": "^4.0.0",
32
+ "@typescript-eslint/parser": "^4.0.0",
33
+ "eslint": "^7.0.0",
34
+ "eslint-config-prettier": "^8.0.0",
35
+ "eslint-plugin-prettier": "^4.0.0",
36
+ "jest": "^26.0.0",
37
+ "prettier": "^2.0.0",
38
+ "ts-jest": "^26.0.0",
39
+ "ts-node": "^10.0.0",
40
+ "typescript": "^4.0.0"
41
+ },
42
+ "keywords": [
43
+ "mongodb",
44
+ "anonymizer"
45
+ ],
46
+ "author": "vanea vasco",
47
+ "license": "MIT"
48
+ }
@@ -0,0 +1,116 @@
1
+ import { faker } from '@faker-js/faker';
2
+
3
+ export class Anonymize {
4
+
5
+ anonymizeBatch(batch: any[], collectionName: string, list: string[]): any[] {
6
+ const keysToAnonymize = this.getKeysToAnonymize(list, collectionName);
7
+ const fieldsToAnonymize = keysToAnonymize.map((item) => item.field);
8
+
9
+ return batch.map((document) => this.anonymizeDocument(document, fieldsToAnonymize, keysToAnonymize));
10
+ }
11
+
12
+ private getKeysToAnonymize(list: string[], collectionName: string) {
13
+ return list
14
+ .filter((item) => !item.match(/^[a-z_]+\./gi) || item.startsWith(`${collectionName}.`))
15
+ .map((item) => ({
16
+ field: item.replace(`${collectionName}.`, "").replace(/:(?:.*)$/, "").toLowerCase(),
17
+ replacement: item.includes(":") ? item.replace(/^(?:.*):/, "") : null,
18
+ }));
19
+ }
20
+
21
+ private anonymizeDocument(document: any, fieldsToAnonymize: string[], keysToAnonymize: any[]) {
22
+ const anonymizedDocument: Record<string, any> = {};
23
+
24
+ for (const key in document) {
25
+ if (!document.hasOwnProperty(key)) continue;
26
+
27
+ if (fieldsToAnonymize.includes(key.toLowerCase())) {
28
+ if (typeof document[key] === 'object') {
29
+ if (Array.isArray(document[key]) && document[key].length > 0) {
30
+ anonymizedDocument[key] = document[key].map((item: any) => this.anonymizeDocument(item, fieldsToAnonymize, keysToAnonymize));
31
+ continue;
32
+ }
33
+
34
+ anonymizedDocument[key] = this.anonymizeDocument(document[key], fieldsToAnonymize, keysToAnonymize);
35
+ continue;
36
+ }
37
+
38
+ anonymizedDocument[key] = this.anonymizeValue(
39
+ key.toLowerCase(),
40
+ keysToAnonymize.find((item) => item.field === key.toLowerCase())?.replacement
41
+ );
42
+ } else {
43
+ anonymizedDocument[key] = document[key];
44
+ }
45
+ }
46
+ return anonymizedDocument;
47
+ }
48
+
49
+ private anonymizeValue(key: string, replacement: string | null) {
50
+ if (replacement) {
51
+ return this.applyReplacement(replacement);
52
+ }
53
+ return this.getFakerValueForField(key);
54
+ }
55
+
56
+ private applyReplacement(replacement: string) {
57
+ if (replacement.startsWith("faker")) {
58
+ return this.getFakerValue(replacement);
59
+ }
60
+ switch (replacement) {
61
+ case "[]": return [];
62
+ case "{}": return {};
63
+ case "null": return null;
64
+ default: {
65
+ if (replacement.startsWith("[") || replacement.startsWith("{")) {
66
+ try {
67
+ return JSON.parse(decodeURIComponent(replacement));
68
+ } catch (error) {
69
+ throw new Error(`Failed to parse replacement JSON: ${(error as Error)?.message}`);
70
+ }
71
+ }
72
+ return replacement;
73
+ }
74
+ }
75
+ }
76
+
77
+ private getFakerValue(replacement: string): any {
78
+ const parts = replacement.split(".");
79
+
80
+ if (parts.length !== 3) {
81
+ throw new Error(`Invalid format for replacement: ${replacement}. Expected format 'faker.category.method'`);
82
+ }
83
+
84
+ const [, category, method] = parts;
85
+ const fakerCategory = (faker as any)[category];
86
+
87
+ if (!fakerCategory) {
88
+ throw new Error(`Invalid faker category: ${category}`);
89
+ }
90
+
91
+ const fakerMethod = fakerCategory[method];
92
+
93
+ if (typeof fakerMethod !== 'function') {
94
+ throw new Error(`Invalid faker method: ${method} in category ${category}`);
95
+ }
96
+
97
+ return fakerMethod();
98
+ }
99
+
100
+
101
+ private getFakerValueForField(key: string) {
102
+ if (key.includes("email")) return faker.internet.email().toLowerCase();
103
+ if (key.includes("firstname")) return faker.person.firstName();
104
+ if (key.includes("lastname")) return faker.person.lastName();
105
+ if (key === "description") return faker.lorem.sentence();
106
+ if (key.endsWith("address")) return faker.location.streetAddress();
107
+ if (key.endsWith("city")) return faker.location.city();
108
+ if (key.endsWith("country")) return faker.location.country();
109
+ if (key.endsWith("phone")) return faker.phone.number();
110
+ if (key.endsWith("comment")) return faker.lorem.sentence();
111
+ if (key.endsWith("date")) return faker.date.past();
112
+ if (key.endsWith("name")) return faker.person.fullName();
113
+
114
+ return faker.word.sample();
115
+ }
116
+ }
@@ -0,0 +1,50 @@
1
+ import { MongoClient, Db } from 'mongodb';
2
+ import * as bunyan from 'bunyan';
3
+
4
+ const log = bunyan.createLogger({ name: "DatabaseHandler" });
5
+
6
+ export class Database {
7
+ private client: MongoClient | null = null;
8
+ private db: Db | null = null;
9
+
10
+ constructor(private uri: string, private name: string) {}
11
+
12
+ async connect() {
13
+ try {
14
+ log.info(`Connecting to ${this.name} database...`);
15
+ this.client = new MongoClient(this.uri);
16
+ await this.client.connect();
17
+ this.db = this.client.db();
18
+ log.info(`Successfully connected to ${this.name} database`);
19
+ } catch (error) {
20
+ throw new Error(`Failed to connect to ${this.name} database: ${(error as Error).message}`);
21
+ }
22
+ }
23
+
24
+ async close() {
25
+ await this?.client?.close();
26
+ log.info(`Closed database connection`);
27
+ }
28
+
29
+ async getCollections(): Promise<string[]> {
30
+ const collections = await this?.db?.listCollections().toArray();
31
+ return collections ? collections.map((item) => item.name) : [];
32
+ }
33
+
34
+ getCursor(collectionName: string) {
35
+ return this.db ? this.db.collection(collectionName).find() : null;
36
+ }
37
+
38
+ async insertAnonymizedBatch(collectionName: string, anonymizedBatch: any[]) {
39
+ if (this.db) {
40
+ await this.db.collection(collectionName).insertMany(anonymizedBatch);
41
+ }
42
+ }
43
+
44
+ async dropCollectionIfExists(collectionName: string) {
45
+ if (this.db && await this.db.collection(collectionName).countDocuments() > 0) {
46
+ await this.db.collection(collectionName).drop();
47
+ log.info(`Dropped existing collection: ${collectionName}`);
48
+ }
49
+ }
50
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './main';
3
+
4
+ main().catch((error) => {console.log(error.message)});
package/src/main.ts ADDED
@@ -0,0 +1,98 @@
1
+ import {Database} from './anonymization/database';
2
+ import {Anonymize} from './anonymization/anonymize';
3
+ import {getFieldList} from './utils/field-utils';
4
+
5
+ const log = require('bunyan').createLogger({name: "Main"});
6
+
7
+ interface Config {
8
+ sourceUri: string;
9
+ targetUri: string;
10
+ fieldList: string[];
11
+ ignoreCollections: string[];
12
+ collectionList: string[];
13
+ batchSize: number;
14
+ copyNonAnonymized: boolean;
15
+ }
16
+
17
+ const defaultFields = ['email', 'name', 'description', 'address', 'city', 'country', 'phone', 'comment', 'birthdate', 'firstname', 'lastname', 'fullname'];
18
+
19
+ export async function main() {
20
+ const config: Config = parseArgs(process.argv);
21
+
22
+ const sourceDb = new Database(config.sourceUri, 'source');
23
+ const targetDb = new Database(config.targetUri, 'target');
24
+ const anonymizer = new Anonymize();
25
+
26
+ try {
27
+ await sourceDb.connect();
28
+ await targetDb.connect();
29
+
30
+ const collections = await sourceDb.getCollections();
31
+
32
+ for (const collectionName of collections) {
33
+ const copyNonAnonymized = config.copyNonAnonymized && (!config.collectionList.includes(collectionName) || config.ignoreCollections.includes(collectionName));
34
+
35
+ if (config.ignoreCollections.includes(collectionName) && !config.copyNonAnonymized) {
36
+ log.info(`Skipping collection anonymization for ${collectionName}. Collection is in ignore list.`);
37
+ continue;
38
+ }
39
+
40
+ if (config.collectionList.length > 0 && !config.collectionList.includes(collectionName) && !config.copyNonAnonymized) {
41
+ log.info(`Skipping collection anonymization for ${collectionName}. Collection is not in the collection list.`);
42
+ continue;
43
+ }
44
+
45
+ if (!copyNonAnonymized) {
46
+ log.info(`Anonymizing collection ${collectionName}.`);
47
+ } else {
48
+ log.info(`Copying collection ${collectionName} without anonymization.`);
49
+ }
50
+ const cursor = sourceDb.getCursor(collectionName);
51
+
52
+ while (await cursor?.hasNext()) {
53
+ const batch = await getBatch(cursor, config.batchSize);
54
+ const anonymizedBatch = !copyNonAnonymized ? anonymizer.anonymizeBatch(batch, collectionName, config.fieldList) : batch;
55
+ await targetDb.insertAnonymizedBatch(collectionName, anonymizedBatch);
56
+ }
57
+ }
58
+
59
+ log.info("Anonymization process completed successfully!");
60
+ } catch (error) {
61
+ log.error(`An error occurred during the anonymization process: ${(error as Error).message}`);
62
+ } finally {
63
+ await sourceDb.close();
64
+ await targetDb.close();
65
+ }
66
+ }
67
+
68
+ function parseArgs(argv: string[]): Config {
69
+ const yargs = require('yargs/yargs');
70
+ const {hideBin} = require('yargs/helpers');
71
+ const args = yargs(hideBin(argv))
72
+ .option('sourceUri', {type: 'string', demandOption: true})
73
+ .option('targetUri', {type: 'string', demandOption: true})
74
+ .option('fieldList', {type: 'string', demandOption: true, default: defaultFields.join(',')})
75
+ .option('collectionList', {type: 'string'})
76
+ .option('ignoreCollections', {type: 'string'})
77
+ .option('batchSize', {type: 'number', default: 1000})
78
+ .option('copyNonAnonymized', {type: 'boolean', default: false})
79
+ .argv;
80
+
81
+ return {
82
+ sourceUri: args.sourceUri,
83
+ targetUri: args.targetUri,
84
+ fieldList: getFieldList(args.fieldList, defaultFields),
85
+ ignoreCollections: args.ignoreCollections?.split(',') || [],
86
+ collectionList: args.collectionList?.split(',') || [],
87
+ batchSize: args.batchSize,
88
+ copyNonAnonymized: args.copyNonAnonymized
89
+ };
90
+ }
91
+
92
+ async function getBatch(cursor: any, batchSize: number) {
93
+ const batch = [];
94
+ for (let i = 0; i < batchSize && await cursor.hasNext(); i++) {
95
+ batch.push(await cursor.next());
96
+ }
97
+ return batch;
98
+ }
@@ -0,0 +1,16 @@
1
+ export function getFieldList(fieldListOption: string, defaultFields: string[] = []) {
2
+ if (fieldListOption.length) {
3
+ if (fieldListOption.startsWith('+')) {
4
+ return defaultFields.concat(fieldListOption.slice(1).split(','));
5
+ }
6
+
7
+ if (fieldListOption.startsWith('-')) {
8
+ const fieldsToRemove = fieldListOption.slice(1).split(',');
9
+ return defaultFields.filter(field => !fieldsToRemove.includes(field));
10
+ }
11
+
12
+ return fieldListOption.split(',');
13
+ }
14
+
15
+ return [...defaultFields];
16
+ }
@@ -0,0 +1,164 @@
1
+ import { Anonymize } from '../../src/anonymization/anonymize';
2
+
3
+ describe('Anonymize', () => {
4
+ let anonymize: Anonymize;
5
+
6
+ beforeEach(() => {
7
+ anonymize = new Anonymize();
8
+ });
9
+
10
+ describe('anonymizeBatch', () => {
11
+ it('should anonymize fields in a batch of documents', () => {
12
+ const batch = [
13
+ { firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com' },
14
+ { firstName: 'Jane', lastName: 'Smith', email: 'jane.smith@example.com' }
15
+ ];
16
+ const collectionName = 'users';
17
+ const list = ['users.firstName', 'users.lastName', 'users.email'];
18
+
19
+ const result = anonymize.anonymizeBatch(batch, collectionName, list);
20
+
21
+ expect(result).toHaveLength(2);
22
+ expect(result[0].firstName).not.toBe('John');
23
+ expect(result[0].lastName).not.toBe('Doe');
24
+ expect(result[0].email).not.toBe('john.doe@example.com');
25
+ expect(result[1].firstName).not.toBe('Jane');
26
+ expect(result[1].lastName).not.toBe('Smith');
27
+ expect(result[1].email).not.toBe('jane.smith@example.com');
28
+ });
29
+ });
30
+
31
+ describe('getKeysToAnonymize', () => {
32
+ it('should filter and map the list of fields to anonymize', () => {
33
+ const list = ['users.firstName', 'users.lastName', 'users.email:faker.internet.email'];
34
+ const collectionName = 'users';
35
+
36
+ // @ts-ignore: private method
37
+ const result = anonymize.getKeysToAnonymize(list, collectionName);
38
+
39
+ expect(result).toEqual([
40
+ { field: 'firstname', replacement: null },
41
+ { field: 'lastname', replacement: null },
42
+ { field: 'email', replacement: 'faker.internet.email' }
43
+ ]);
44
+ });
45
+ });
46
+
47
+ describe('anonymizeDocument', () => {
48
+ it('should anonymize specified fields in a document', () => {
49
+ const document = { firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com' };
50
+ const fieldsToAnonymize = ['firstname', 'lastname', 'email'];
51
+ const keysToAnonymize = [
52
+ { field: 'firstname', replacement: null },
53
+ { field: 'lastname', replacement: null },
54
+ { field: 'email', replacement: 'faker.internet.email' }
55
+ ];
56
+
57
+ // @ts-ignore: private method
58
+ const result = anonymize.anonymizeDocument(document, fieldsToAnonymize, keysToAnonymize);
59
+
60
+ expect(result.firstName).not.toBe('John');
61
+ expect(result.lastName).not.toBe('Doe');
62
+ expect(result.email).not.toBe('john.doe@example.com');
63
+ });
64
+ });
65
+
66
+ describe('anonymizeValue', () => {
67
+ it('should apply replacement if provided', () => {
68
+ // @ts-ignore: private method
69
+ const result = anonymize.anonymizeValue('email', 'faker.internet.email');
70
+ expect(result).not.toBe(null);
71
+ });
72
+
73
+ it('should use faker value if no replacement is provided', () => {
74
+ // @ts-ignore: private method
75
+ const result = anonymize.anonymizeValue('email', null);
76
+ expect(result).not.toBe(null);
77
+ });
78
+ });
79
+
80
+ describe('applyReplacement', () => {
81
+ it('should handle faker replacements', () => {
82
+ // @ts-ignore: private method
83
+ const result = anonymize.applyReplacement('faker.internet.email');
84
+ expect(result).not.toBe(null);
85
+ });
86
+
87
+ it('should return empty array for [] replacement', () => {
88
+ // @ts-ignore: private method
89
+ const result = anonymize.applyReplacement('[]');
90
+ expect(result).toEqual([]);
91
+ });
92
+
93
+ it('should return empty object for {} replacement', () => {
94
+ // @ts-ignore: private method
95
+ const result = anonymize.applyReplacement('{}');
96
+ expect(result).toEqual({});
97
+ });
98
+
99
+ it('should return null for null replacement', () => {
100
+ // @ts-ignore: private method
101
+ const result = anonymize.applyReplacement('null');
102
+ expect(result).toBeNull();
103
+ });
104
+
105
+ it('should parse and return JSON for valid JSON replacements', () => {
106
+ // @ts-ignore: private method
107
+ const result = anonymize.applyReplacement('{"key":"value"}'); // {"key":"value"}
108
+ expect(result).toEqual({ key: 'value' });
109
+ });
110
+
111
+ it('should throw an error for invalid JSON replacements', () => {
112
+ expect(() => {
113
+ // @ts-ignore: private method
114
+ anonymize.applyReplacement('[invalid-json');
115
+ }).toThrowError( 'Failed to parse replacement JSON: Unexpected token i in JSON at position 1');
116
+ });
117
+ });
118
+
119
+ describe('getFakerValue', () => {
120
+ it('should return a faker value for valid replacement', () => {
121
+ // @ts-ignore: private method
122
+ const result = anonymize.getFakerValue('faker.internet.email');
123
+ expect(result).not.toBe(null);
124
+ });
125
+
126
+ it('should throw an error for invalid faker category', () => {
127
+ expect(() => {
128
+ // @ts-ignore: private method
129
+ anonymize.getFakerValue('faker.invalidCategory.method');
130
+ }).toThrowError('Invalid faker category: invalidCategory');
131
+ });
132
+
133
+ it('should throw an error for invalid faker method', () => {
134
+ expect(() => {
135
+ // @ts-ignore: private method
136
+ anonymize.getFakerValue('faker.internet.invalidMethod');
137
+ }).toThrowError('Invalid faker method: invalidMethod in category internet');
138
+ });
139
+
140
+ it('should throw an error for incorrect format', () => {
141
+ expect(() => {
142
+ // @ts-ignore: private method
143
+ anonymize.getFakerValue('faker.internet');
144
+ }).toThrowError('Invalid format for replacement: faker.internet. Expected format \'faker.category.method\'');
145
+ });
146
+ });
147
+
148
+ describe('getFakerValueForField', () => {
149
+ it('should return correct faker value for recognized keys', () => {
150
+ // @ts-ignore: private method
151
+ expect(anonymize.getFakerValueForField('email')).not.toBe(null);
152
+ // @ts-ignore: private method
153
+ expect(anonymize.getFakerValueForField('firstname')).not.toBe(null);
154
+ // @ts-ignore: private method
155
+ expect(anonymize.getFakerValueForField('lastname')).not.toBe(null);
156
+ });
157
+
158
+ it('should return a sample word for unrecognized keys', () => {
159
+ // @ts-ignore: private method
160
+ const result = anonymize.getFakerValueForField('unrecognizedKey');
161
+ expect(result).not.toBe(null);
162
+ });
163
+ });
164
+ });
@@ -0,0 +1,31 @@
1
+ import {getFieldList} from '../../src/utils/field-utils';
2
+
3
+ describe('getFieldList', () => {
4
+ it('should return the default field list if no field list option is provided', () => {
5
+ const result = getFieldList('', ['field1', 'field2', 'field3']);
6
+ expect(result).toEqual(['field1', 'field2', 'field3']);
7
+ });
8
+
9
+ it('should return an empty array if no filed list is provided and no default list is provided', () => {
10
+ const result = getFieldList('');
11
+ expect(result).toEqual([]);
12
+ });
13
+
14
+ it('should return the correct field list if a field list is provided without modifiers', () => {
15
+ const result = getFieldList('field1,field2,field3', ['field4', 'field5', 'field3']);
16
+
17
+ expect(result).toEqual(['field1', 'field2', 'field3']);
18
+ });
19
+
20
+ it('should return the correct field list if a field list is provided with a + modifier', () => {
21
+ const result = getFieldList('+field1,field2', ['field3', 'field4']);
22
+
23
+ expect(result).toEqual(['field3', 'field4', 'field1', 'field2']);
24
+ });
25
+
26
+ it('should return the correct field list if a field list is provided with a - modifier', () => {
27
+ const result = getFieldList('-field1,field2', ['field1', 'field2', 'field3']);
28
+
29
+ expect(result).toEqual(['field3']);
30
+ });
31
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "target": "es6",
5
+ "declaration": true,
6
+ "sourceMap": true,
7
+ "outDir": "./dist",
8
+ "experimentalDecorators": true,
9
+ "emitDecoratorMetadata": true,
10
+ "strict": true
11
+ },
12
+ "include": ["src/**/*.ts"],
13
+ "exclude": ["node_modules"]
14
+ }