@smartsoft001/mongo 1.1.90 → 1.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/.eslintrc +13 -0
- package/jest.config.ts +27 -0
- package/package.json +2 -26
- package/project.json +48 -0
- package/src/index.ts +3 -0
- package/src/lib/mongo.config.ts +10 -0
- package/src/lib/mongo.module.ts +28 -0
- package/src/lib/mongo.unitofwork.spec.ts +84 -0
- package/src/lib/mongo.unitofwork.ts +58 -0
- package/src/lib/mongo.utils.spec.ts +54 -0
- package/src/lib/mongo.utils.ts +17 -0
- package/src/lib/repositories/attachment.repository.spec.ts +249 -0
- package/src/lib/repositories/attachment.repository.ts +113 -0
- package/src/lib/repositories/interfaces.ts +28 -0
- package/src/lib/repositories/item.repository.spec.ts +1269 -0
- package/src/lib/repositories/item.repository.ts +582 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +11 -0
- package/tsconfig.spec.json +20 -0
- package/src/index.d.ts +0 -3
- package/src/index.js +0 -7
- package/src/index.js.map +0 -1
- package/src/lib/mongo.config.d.ts +0 -9
- package/src/lib/mongo.config.js +0 -7
- package/src/lib/mongo.config.js.map +0 -1
- package/src/lib/mongo.module.d.ts +0 -5
- package/src/lib/mongo.module.js +0 -25
- package/src/lib/mongo.module.js.map +0 -1
- package/src/lib/mongo.unitofwork.d.ts +0 -12
- package/src/lib/mongo.unitofwork.js +0 -56
- package/src/lib/mongo.unitofwork.js.map +0 -1
- package/src/lib/mongo.utils.d.ts +0 -2
- package/src/lib/mongo.utils.js +0 -13
- package/src/lib/mongo.utils.js.map +0 -1
- package/src/lib/repositories/attachment.repository.d.ts +0 -28
- package/src/lib/repositories/attachment.repository.js +0 -88
- package/src/lib/repositories/attachment.repository.js.map +0 -1
- package/src/lib/repositories/item.repository.d.ts +0 -51
- package/src/lib/repositories/item.repository.js +0 -409
- package/src/lib/repositories/item.repository.js.map +0 -1
- package/test-setup.js +0 -1
- package/test-setup.js.map +0 -1
- /package/{test-setup.d.ts → test-setup.ts} +0 -0
package/.eslintrc
ADDED
package/jest.config.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
export default {
|
|
3
|
+
globals: {},
|
|
4
|
+
transform: {
|
|
5
|
+
'^.+\\.[tj]sx?$': [
|
|
6
|
+
'ts-jest',
|
|
7
|
+
{
|
|
8
|
+
tsconfig: '<rootDir>/tsconfig.spec.json',
|
|
9
|
+
},
|
|
10
|
+
],
|
|
11
|
+
},
|
|
12
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
|
13
|
+
coverageDirectory: '../../../coverage/libs/shared/mongo',
|
|
14
|
+
displayName: 'shared-mongo',
|
|
15
|
+
testEnvironment: 'node',
|
|
16
|
+
preset: '../../../jest.preset.js',
|
|
17
|
+
/* TODO: Update to latest Jest snapshotFormat
|
|
18
|
+
* By default Nx has kept the older style of Jest Snapshot formats
|
|
19
|
+
* to prevent breaking of any existing tests with snapshots.
|
|
20
|
+
* It's recommend you update to the latest format.
|
|
21
|
+
* You can do this by removing snapshotFormat property
|
|
22
|
+
* and running tests with --update-snapshot flag.
|
|
23
|
+
* Example: From within the project directory, run "nx test --update-snapshot"
|
|
24
|
+
* More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
|
|
25
|
+
*/
|
|
26
|
+
snapshotFormat: { escapeString: true, printBasicPrototype: true },
|
|
27
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartsoft001/mongo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Utils to mongo",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,29 +16,5 @@
|
|
|
16
16
|
"bugs": {
|
|
17
17
|
"url": "https://github.com/emiljuchnikowski/smartsoft/issues"
|
|
18
18
|
},
|
|
19
|
-
"homepage": "https://github.com/emiljuchnikowski/smartsoft#readme"
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"@angular/common": "16.1.3",
|
|
22
|
-
"@angular/core": "16.1.3",
|
|
23
|
-
"@angular/platform-browser-dynamic": "16.1.3",
|
|
24
|
-
"@nestjs/common": "^10.0.5",
|
|
25
|
-
"flatted": "^3.2.4",
|
|
26
|
-
"guid-typescript": "1.0.9",
|
|
27
|
-
"lodash": "^4.17.20",
|
|
28
|
-
"md5": "2.3.0",
|
|
29
|
-
"mongodb": "5.6.0",
|
|
30
|
-
"reflect-metadata": "0.1.13",
|
|
31
|
-
"rxjs": "7.8.1"
|
|
32
|
-
},
|
|
33
|
-
"peerDependencies": {
|
|
34
|
-
"@smartsoft001/crud-shell-dtos": "1.1.90",
|
|
35
|
-
"@smartsoft001/domain-core": "1.1.90",
|
|
36
|
-
"@smartsoft001/models": "1.1.90",
|
|
37
|
-
"@smartsoft001/users": "1.1.90",
|
|
38
|
-
"@smartsoft001/utils": "1.1.90",
|
|
39
|
-
"util": "0.12.5",
|
|
40
|
-
"tslib": "2.5.3"
|
|
41
|
-
},
|
|
42
|
-
"main": "./src/index.js",
|
|
43
|
-
"types": "./src/index.d.ts"
|
|
19
|
+
"homepage": "https://github.com/emiljuchnikowski/smartsoft#readme"
|
|
44
20
|
}
|
package/project.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shared-mongo",
|
|
3
|
+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "packages/shared/mongo/src",
|
|
5
|
+
"projectType": "library",
|
|
6
|
+
"generators": {},
|
|
7
|
+
"targets": {
|
|
8
|
+
"lint": {
|
|
9
|
+
"executor": "@nx/eslint:lint",
|
|
10
|
+
"outputs": ["{options.outputFile}"],
|
|
11
|
+
"options": {
|
|
12
|
+
"lintFilePatterns": [
|
|
13
|
+
"packages/shared/mongo/**/*.{ts,tsx,js,jsx}",
|
|
14
|
+
"packages/shared/mongo/package.json"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"test": {
|
|
19
|
+
"executor": "@nx/jest:jest",
|
|
20
|
+
"options": {
|
|
21
|
+
"jestConfig": "packages/shared/mongo/jest.config.ts"
|
|
22
|
+
},
|
|
23
|
+
"outputs": ["{workspaceRoot}/coverage/packages/shared/mongo"]
|
|
24
|
+
},
|
|
25
|
+
"build": {
|
|
26
|
+
"executor": "@nx/esbuild:esbuild",
|
|
27
|
+
"outputs": ["{options.outputPath}"],
|
|
28
|
+
"options": {
|
|
29
|
+
"outputPath": "dist/packages/shared/mongo",
|
|
30
|
+
"tsConfig": "packages/shared/mongo/tsconfig.lib.json",
|
|
31
|
+
"packageJson": "packages/shared/mongo/package.json",
|
|
32
|
+
"main": "packages/shared/mongo/src/index.ts",
|
|
33
|
+
"assets": ["packages/shared/mongo/*.md"],
|
|
34
|
+
"generatePackageJson": true,
|
|
35
|
+
"format": ["cjs"]
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"deploy": {
|
|
39
|
+
"executor": "ngx-deploy-npm:deploy",
|
|
40
|
+
"options": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"distFolderPath": "dist/packages/shared/mongo"
|
|
43
|
+
},
|
|
44
|
+
"dependsOn": ["build"]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"tags": ["scope:shared", "type:util"]
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
IAttachmentRepository,
|
|
4
|
+
IItemRepository,
|
|
5
|
+
IUnitOfWork,
|
|
6
|
+
} from '@smartsoft001/domain-core';
|
|
7
|
+
|
|
8
|
+
import { MongoConfig } from './mongo.config';
|
|
9
|
+
import { MongoUnitOfWork } from './mongo.unitofwork';
|
|
10
|
+
import { MongoAttachmentRepository } from './repositories/attachment.repository';
|
|
11
|
+
import { MongoItemRepository } from './repositories/item.repository';
|
|
12
|
+
|
|
13
|
+
export class MongoModule {
|
|
14
|
+
static forRoot(config: MongoConfig): DynamicModule {
|
|
15
|
+
const providers = [
|
|
16
|
+
{ provide: MongoConfig, useValue: config },
|
|
17
|
+
{ provide: IItemRepository, useClass: MongoItemRepository },
|
|
18
|
+
{ provide: IAttachmentRepository, useClass: MongoAttachmentRepository },
|
|
19
|
+
{ provide: IUnitOfWork, useClass: MongoUnitOfWork },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
module: MongoModule,
|
|
24
|
+
providers: providers,
|
|
25
|
+
exports: providers,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import { MongoConfig } from '@smartsoft001/mongo';
|
|
3
|
+
import { MongoClient, ClientSession, TransactionOptions } from 'mongodb';
|
|
4
|
+
|
|
5
|
+
import { IMongoTransaction, MongoUnitOfWork } from './mongo.unitofwork';
|
|
6
|
+
|
|
7
|
+
const mockData: {
|
|
8
|
+
config: MongoConfig;
|
|
9
|
+
url: string;
|
|
10
|
+
} = {
|
|
11
|
+
config: {
|
|
12
|
+
host: 'host',
|
|
13
|
+
port: 4200,
|
|
14
|
+
database: 'db-string',
|
|
15
|
+
},
|
|
16
|
+
url: 'mongodb://host:4200?authSource=db-string',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('shared-mongo: MongoUnitOfWork scope function', () => {
|
|
20
|
+
let mockClient: Partial<MongoClient>;
|
|
21
|
+
let mockSession: Partial<ClientSession>;
|
|
22
|
+
let model: MongoUnitOfWork;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
model = new MongoUnitOfWork(mockData.config);
|
|
26
|
+
|
|
27
|
+
mockSession = {
|
|
28
|
+
withTransaction: jest.fn(
|
|
29
|
+
async <T>(
|
|
30
|
+
callback: (session: ClientSession) => Promise<T>,
|
|
31
|
+
options: TransactionOptions,
|
|
32
|
+
): Promise<T> => {
|
|
33
|
+
return callback(mockSession as ClientSession); // Pass the mocked session
|
|
34
|
+
},
|
|
35
|
+
) as ClientSession['withTransaction'],
|
|
36
|
+
endSession: jest.fn(() => Promise.resolve()),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
mockClient = {
|
|
40
|
+
startSession: jest.fn(() => mockSession as ClientSession),
|
|
41
|
+
close: jest.fn(() => Promise.resolve()),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
jest
|
|
45
|
+
.spyOn(MongoClient, 'connect')
|
|
46
|
+
.mockResolvedValue(mockClient as MongoClient);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
jest.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should execute the definition within a transaction', async () => {
|
|
54
|
+
const mockDefinition = jest.fn(async (transaction: IMongoTransaction) => {
|
|
55
|
+
expect(transaction.session).toBe(mockSession);
|
|
56
|
+
expect(transaction.connection).toBe(mockClient);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await expect(model.scope(mockDefinition)).resolves.not.toThrow();
|
|
60
|
+
|
|
61
|
+
expect(MongoClient.connect).toHaveBeenCalledWith(mockData.url);
|
|
62
|
+
expect(mockClient.startSession).toHaveBeenCalled();
|
|
63
|
+
expect(mockSession.withTransaction).toHaveBeenCalled();
|
|
64
|
+
expect(mockDefinition).toHaveBeenCalled();
|
|
65
|
+
expect(mockSession.endSession).toHaveBeenCalled();
|
|
66
|
+
expect(mockClient.close).toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should throw an error if the definition throws an error', async () => {
|
|
70
|
+
const mockError = new Error('Test error');
|
|
71
|
+
const mockDefinition = jest.fn(async () => {
|
|
72
|
+
throw mockError;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await expect(model.scope(mockDefinition)).rejects.toThrow(mockError);
|
|
76
|
+
|
|
77
|
+
expect(MongoClient.connect).toHaveBeenCalledWith(mockData.url);
|
|
78
|
+
expect(mockClient.startSession).toHaveBeenCalled();
|
|
79
|
+
expect(mockSession.withTransaction).toHaveBeenCalled();
|
|
80
|
+
expect(mockDefinition).toHaveBeenCalled();
|
|
81
|
+
expect(mockSession.endSession).toHaveBeenCalled();
|
|
82
|
+
expect(mockClient.close).toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ClientSession, MongoClient, TransactionOptions } from 'mongodb';
|
|
3
|
+
|
|
4
|
+
import { ITransaction, IUnitOfWork } from '@smartsoft001/domain-core';
|
|
5
|
+
import { MongoConfig } from '@smartsoft001/mongo';
|
|
6
|
+
|
|
7
|
+
import { getMongoUrl } from './mongo.utils';
|
|
8
|
+
|
|
9
|
+
export interface IMongoTransaction extends ITransaction {
|
|
10
|
+
session: ClientSession;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class MongoUnitOfWork extends IUnitOfWork {
|
|
15
|
+
constructor(private config: MongoConfig) {
|
|
16
|
+
super();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async scope(
|
|
20
|
+
definition: (transaction: ITransaction) => Promise<void>,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const client = await MongoClient.connect(this.getUrl());
|
|
23
|
+
|
|
24
|
+
// Step 1: Start a Client Session
|
|
25
|
+
const session = client.startSession();
|
|
26
|
+
|
|
27
|
+
// Step 2: Optional. Define options to use for the transaction
|
|
28
|
+
const transactionOptions: TransactionOptions = {
|
|
29
|
+
readPreference: 'primary',
|
|
30
|
+
readConcern: { level: 'local' },
|
|
31
|
+
writeConcern: { w: 'majority' },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Step 3: Use withTransaction to start a transaction, execute the callback, and commit (or abort on error)
|
|
35
|
+
// Note: The callback for withTransaction MUST be async and/or return a Promise.
|
|
36
|
+
|
|
37
|
+
let error = null;
|
|
38
|
+
try {
|
|
39
|
+
await session.withTransaction(async () => {
|
|
40
|
+
await definition({
|
|
41
|
+
session,
|
|
42
|
+
connection: client,
|
|
43
|
+
} as IMongoTransaction);
|
|
44
|
+
}, transactionOptions);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
error = e;
|
|
47
|
+
} finally {
|
|
48
|
+
await session.endSession();
|
|
49
|
+
await client.close();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (error) throw error;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private getUrl(): string {
|
|
56
|
+
return getMongoUrl(this.config);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { MongoConfig } from '@smartsoft001/mongo';
|
|
2
|
+
|
|
3
|
+
import { getMongoUrl } from './mongo.utils';
|
|
4
|
+
|
|
5
|
+
const mockData: (MongoConfig & { result: string; testName: string })[] = [
|
|
6
|
+
{
|
|
7
|
+
host: 'host',
|
|
8
|
+
port: 4200,
|
|
9
|
+
database: 'db-string',
|
|
10
|
+
result: 'mongodb://host:4200?authSource=db-string',
|
|
11
|
+
testName: 'Basic',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
host: 'ondigitalocean.com',
|
|
15
|
+
port: 4200,
|
|
16
|
+
database: 'db-string',
|
|
17
|
+
result:
|
|
18
|
+
'mongodb+srv://ondigitalocean.com:4200?authSource=db-string&tls=true',
|
|
19
|
+
testName: 'Specific host',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
host: 'host',
|
|
23
|
+
port: 4200,
|
|
24
|
+
database: 'db-string',
|
|
25
|
+
result: 'mongodb://host:4200?authSource=db-string',
|
|
26
|
+
testName: 'Only username',
|
|
27
|
+
username: 'username',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
host: 'host',
|
|
31
|
+
port: 4200,
|
|
32
|
+
database: 'db-string',
|
|
33
|
+
result: 'mongodb://host:4200?authSource=db-string',
|
|
34
|
+
testName: 'Only password',
|
|
35
|
+
password: 'password',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
host: 'host',
|
|
39
|
+
port: 4200,
|
|
40
|
+
database: 'db-string',
|
|
41
|
+
result: 'mongodb://username:password@host:4200?authSource=db-string',
|
|
42
|
+
testName: 'Username and Password',
|
|
43
|
+
username: 'username',
|
|
44
|
+
password: 'password',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
describe('shared-mongo: utils getMongoUrl function', () => {
|
|
49
|
+
for (const data of mockData) {
|
|
50
|
+
it(data.testName, () => {
|
|
51
|
+
expect(getMongoUrl(data)).toBe(data.result);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MongoConfig } from '@smartsoft001/mongo';
|
|
2
|
+
|
|
3
|
+
export function getMongoUrl(config: MongoConfig): string {
|
|
4
|
+
let url;
|
|
5
|
+
if (config.username && config.password)
|
|
6
|
+
url = `mongodb://${config.username}:${config.password}@${config.host}:${config.port}`;
|
|
7
|
+
else url = `mongodb://${config.host}:${config.port}`;
|
|
8
|
+
|
|
9
|
+
url = url + '?authSource=' + config.database;
|
|
10
|
+
|
|
11
|
+
if (config.host.indexOf('ondigitalocean.com') > -1) {
|
|
12
|
+
url = url.replace('mongodb://', 'mongodb+srv://');
|
|
13
|
+
url = url + '&tls=true';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return url;
|
|
17
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { MongoClient, GridFSBucket } from 'mongodb';
|
|
2
|
+
import { Readable } from 'stream';
|
|
3
|
+
|
|
4
|
+
import { IEntity } from '@smartsoft001/domain-core';
|
|
5
|
+
|
|
6
|
+
import { MongoAttachmentRepository } from './attachment.repository';
|
|
7
|
+
|
|
8
|
+
jest.mock('mongodb', () => {
|
|
9
|
+
const mockOpenDownloadStream = jest.fn();
|
|
10
|
+
const mockDelete = jest.fn();
|
|
11
|
+
const mockClose = jest.fn();
|
|
12
|
+
const mockToArray = jest.fn();
|
|
13
|
+
const mockFind = jest.fn().mockReturnValue({
|
|
14
|
+
toArray: mockToArray, // Return a mock cursor with toArray method
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const mockWriteStream = {
|
|
18
|
+
on: jest.fn((event, callback) => {
|
|
19
|
+
if (event === 'finish') {
|
|
20
|
+
setTimeout(callback, 0); // Simulate the 'finish' event after a timeout
|
|
21
|
+
} else if (event === 'error') {
|
|
22
|
+
// You can use this to simulate an error event in your test
|
|
23
|
+
mockWriteStream._errorCallback = callback;
|
|
24
|
+
}
|
|
25
|
+
return mockWriteStream; // Return this for chaining
|
|
26
|
+
}),
|
|
27
|
+
pipe: jest.fn(function () {
|
|
28
|
+
return this; // Make sure pipe is chainable
|
|
29
|
+
}),
|
|
30
|
+
once: jest.fn((event, callback) => {
|
|
31
|
+
// Handle 'once' event similarly to 'on'
|
|
32
|
+
if (event === 'finish') {
|
|
33
|
+
setTimeout(callback, 0); // Simulate the 'finish' event after a timeout
|
|
34
|
+
}
|
|
35
|
+
return mockWriteStream; // Chainable
|
|
36
|
+
}),
|
|
37
|
+
emit: jest.fn((event, ...args) => {
|
|
38
|
+
if (event === 'finish') {
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
41
|
+
mockWriteStream.on('finish', () => {
|
|
42
|
+
// Simulate the finish event
|
|
43
|
+
});
|
|
44
|
+
}, 0);
|
|
45
|
+
}
|
|
46
|
+
return true; // Return true for emit as a valid event
|
|
47
|
+
}),
|
|
48
|
+
removeListener: jest.fn((event, callback) => {
|
|
49
|
+
// Just return the mock itself for chaining
|
|
50
|
+
return mockWriteStream;
|
|
51
|
+
}),
|
|
52
|
+
_errorCallback: null, // Used to simulate an error when you trigger it manually
|
|
53
|
+
};
|
|
54
|
+
const mockOpenUploadStreamWithId = jest.fn().mockReturnValue(mockWriteStream);
|
|
55
|
+
const mockGridFSBucket = jest.fn().mockImplementation(() => ({
|
|
56
|
+
delete: mockDelete,
|
|
57
|
+
openDownloadStream: mockOpenDownloadStream,
|
|
58
|
+
find: mockFind,
|
|
59
|
+
openUploadStreamWithId: mockOpenUploadStreamWithId,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
const mockDb = jest.fn().mockReturnValue({
|
|
63
|
+
GridFSBucket: mockGridFSBucket,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const mockConnect = jest.fn().mockResolvedValue({
|
|
67
|
+
db: jest.fn(() => mockDb()),
|
|
68
|
+
close: mockClose,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
MongoClient: {
|
|
73
|
+
connect: mockConnect,
|
|
74
|
+
},
|
|
75
|
+
GridFSBucket: mockGridFSBucket,
|
|
76
|
+
__mockDelete: mockDelete, // Expose the mockDelete for assertions
|
|
77
|
+
__mockClose: mockClose, // Expose the mockClose for assertions
|
|
78
|
+
__mockOpenDownloadStream: mockOpenDownloadStream, // Expose the mockOpenDownloadStream for assertions
|
|
79
|
+
__mockToArray: mockToArray, // Expose the mockToArray for assertions
|
|
80
|
+
__mockWriteStream: mockWriteStream, // Expose the mockWriteStream for assertions
|
|
81
|
+
__mockOpenUploadStreamWithId: mockOpenUploadStreamWithId, // Expose the mockOpenUploadStreamWithId for assertions
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('shared-mongo: MongoAttachmentRepository', () => {
|
|
86
|
+
let model: MongoAttachmentRepository<IEntity<string>>;
|
|
87
|
+
const mockConfig = {
|
|
88
|
+
database: 'testDB',
|
|
89
|
+
host: 'host',
|
|
90
|
+
port: 4200,
|
|
91
|
+
collection: 'testCollection',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const data = {
|
|
95
|
+
id: 'test-id',
|
|
96
|
+
fileName: 'test-file.txt',
|
|
97
|
+
stream: new Readable(),
|
|
98
|
+
mimeType: 'text/plain',
|
|
99
|
+
encoding: 'utf8',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const options = {
|
|
103
|
+
streamCallback: jest.fn(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Declare mockDelete here to access in the test scope
|
|
107
|
+
const mongodbMock = jest.requireMock('mongodb');
|
|
108
|
+
const mockDelete = mongodbMock.__mockDelete;
|
|
109
|
+
const mockClose = mongodbMock.__mockClose;
|
|
110
|
+
const mockOpenDownloadStream = mongodbMock.__mockOpenDownloadStream;
|
|
111
|
+
const mockToArray = mongodbMock.__mockToArray;
|
|
112
|
+
const mockWriteStream = mongodbMock.__mockWriteStream;
|
|
113
|
+
const mockOpenUploadStreamWithId = mongodbMock.__mockOpenUploadStreamWithId;
|
|
114
|
+
const mockStream = new Readable();
|
|
115
|
+
mockStream._read = jest.fn();
|
|
116
|
+
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
model = new MongoAttachmentRepository<IEntity<string>>(mockConfig); // Pass required config
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('delete() should delete a file from GridFSBucket and close the client', async () => {
|
|
122
|
+
const id = 'test-id';
|
|
123
|
+
const mockUrl = 'mock-url';
|
|
124
|
+
jest.spyOn(model as any, 'getUrl').mockImplementation(() => mockUrl);
|
|
125
|
+
|
|
126
|
+
await model.delete(id);
|
|
127
|
+
|
|
128
|
+
// Assertions
|
|
129
|
+
expect(MongoClient.connect).toHaveBeenCalledWith(mockUrl);
|
|
130
|
+
expect(GridFSBucket).toHaveBeenCalledWith(expect.anything(), {
|
|
131
|
+
bucketName: mockConfig.collection,
|
|
132
|
+
});
|
|
133
|
+
expect(mockDelete).toHaveBeenCalledWith(id);
|
|
134
|
+
expect(mockClose).toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('getStream() should return a readable stream when getStream is called', async () => {
|
|
138
|
+
const id = 'test-id';
|
|
139
|
+
const options = { start: 0, end: 100 };
|
|
140
|
+
const mockUrl = 'mock-url';
|
|
141
|
+
jest.spyOn(model as any, 'getUrl').mockImplementation(() => mockUrl);
|
|
142
|
+
|
|
143
|
+
// Mock the return value of openDownloadStream to return a mock Readable stream
|
|
144
|
+
const mockStream = new Readable();
|
|
145
|
+
mockStream._read = jest.fn(); // Implement the _read method to make it a valid Readable stream
|
|
146
|
+
mockOpenDownloadStream.mockReturnValue(mockStream);
|
|
147
|
+
|
|
148
|
+
const stream = await model.getStream(id, options);
|
|
149
|
+
|
|
150
|
+
// Assertions
|
|
151
|
+
expect(MongoClient.connect).toHaveBeenCalledWith(mockUrl); // Ensure MongoClient.connect is called with the correct URL
|
|
152
|
+
expect(mockOpenDownloadStream).toHaveBeenCalledWith(id, options); // Check if openDownloadStream was called with the right parameters
|
|
153
|
+
expect(stream).toBe(mockStream); // Ensure the returned stream is the one we mocked
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('getInfo() should return file info if the file exists', async () => {
|
|
157
|
+
const id = 'test-id';
|
|
158
|
+
const mockFileData = [
|
|
159
|
+
{
|
|
160
|
+
filename: 'test-file.txt',
|
|
161
|
+
contentType: 'text/plain',
|
|
162
|
+
length: 1024,
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
const mockUrl = 'mock-url';
|
|
166
|
+
|
|
167
|
+
mockToArray.mockResolvedValueOnce(mockFileData); // Mock toArray to return mock file data
|
|
168
|
+
|
|
169
|
+
const result = await model.getInfo(id);
|
|
170
|
+
|
|
171
|
+
expect(MongoClient.connect).toHaveBeenCalledWith(mockUrl); // Ensure MongoClient.connect is called with the correct URL
|
|
172
|
+
expect(result).toEqual({
|
|
173
|
+
fileName: 'test-file.txt',
|
|
174
|
+
contentType: 'text/plain',
|
|
175
|
+
length: 1024,
|
|
176
|
+
}); // Check if the correct file info is returned
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('getInfo() should return null if no file is found', async () => {
|
|
180
|
+
const id = 'non-existent-id';
|
|
181
|
+
|
|
182
|
+
// Mock the return value of toArray to return an empty array
|
|
183
|
+
(MongoClient.connect as jest.Mock).mockResolvedValueOnce({
|
|
184
|
+
db: jest.fn(() => ({
|
|
185
|
+
GridFSBucket: jest.fn().mockImplementation(() => ({
|
|
186
|
+
find: jest.fn().mockReturnValue({
|
|
187
|
+
toArray: jest.fn().mockResolvedValue([]), // Return mock file data
|
|
188
|
+
}),
|
|
189
|
+
})),
|
|
190
|
+
})),
|
|
191
|
+
close: jest.fn(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const result = await model.getInfo(id);
|
|
195
|
+
|
|
196
|
+
expect(result).toBeNull(); // Expect null if no file is found
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// it("upload() should upload file successfully", async () => {
|
|
200
|
+
// const mockUrl = "mock-url";
|
|
201
|
+
// jest.spyOn(model as any, "getUrl").mockImplementation(() => mockUrl);
|
|
202
|
+
//
|
|
203
|
+
// const mockStream = new Readable();
|
|
204
|
+
// mockStream._read = jest.fn();
|
|
205
|
+
//
|
|
206
|
+
// // Set up the stream to simulate data being piped
|
|
207
|
+
// data.stream = mockStream;
|
|
208
|
+
//
|
|
209
|
+
// const result = model.upload(data);
|
|
210
|
+
//
|
|
211
|
+
// // Ensure the MongoClient.connect method is called with the correct URL
|
|
212
|
+
// expect(MongoClient.connect).toHaveBeenCalledWith(mockUrl);
|
|
213
|
+
//
|
|
214
|
+
// // Wait for the upload promise to resolve
|
|
215
|
+
// await expect(result).resolves.toBeUndefined();
|
|
216
|
+
//
|
|
217
|
+
// // Ensure GridFSBucket and openUploadStreamWithId are called
|
|
218
|
+
// expect(GridFSBucket).toHaveBeenCalledWith(expect.anything(), {
|
|
219
|
+
// bucketName: mockConfig.collection,
|
|
220
|
+
// });
|
|
221
|
+
// expect(mockOpenUploadStreamWithId).toHaveBeenCalledWith(data.id, data.fileName, {
|
|
222
|
+
// contentType: data.mimeType,
|
|
223
|
+
// });
|
|
224
|
+
//
|
|
225
|
+
// // Ensure the stream callback is called if provided
|
|
226
|
+
// expect(options.streamCallback).toHaveBeenCalledWith(mockWriteStream);
|
|
227
|
+
//
|
|
228
|
+
// // Ensure data is piped into the write stream
|
|
229
|
+
// expect(mockStream.pipe).toHaveBeenCalledWith(mockWriteStream);
|
|
230
|
+
// });
|
|
231
|
+
|
|
232
|
+
// it("upload() should reject with error if upload fails", async () => {
|
|
233
|
+
// const mockUrl = "mock-url";
|
|
234
|
+
// jest.spyOn(model as any, "getUrl").mockImplementation(() => mockUrl);
|
|
235
|
+
//
|
|
236
|
+
// const result = model.upload({
|
|
237
|
+
// ...data,
|
|
238
|
+
// stream: mockStream
|
|
239
|
+
// });
|
|
240
|
+
//
|
|
241
|
+
// if (mockWriteStream._errorCallback) {
|
|
242
|
+
// mockWriteStream._errorCallback(new Error("Upload failed"));
|
|
243
|
+
// }
|
|
244
|
+
// // Ensure the upload rejects with the error
|
|
245
|
+
// // await expect(result.catch(e => e)).rejects.toThrowError("Upload failed");
|
|
246
|
+
//
|
|
247
|
+
// await expect(result).rejects.toThrowError("Upload failed");
|
|
248
|
+
// });
|
|
249
|
+
});
|