Исправлены ошибки сборки проекта
This commit is contained in:
parent
326b97931c
commit
eb16e92a6c
27 changed files with 284 additions and 327 deletions
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
|
|
@ -8,10 +8,10 @@
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Launch Program",
|
"name": "Launch Program",
|
||||||
"runtimeExecutable": "yarn",
|
"runtimeExecutable": "npm",
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
"start:debug"
|
"run", "start:debug"
|
||||||
],
|
],
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { CouchDb } from './couchdb-datasources/couchdb';
|
||||||
import { Users } from './couchdb-datasources/users';
|
import { Users } from './couchdb-datasources/users';
|
||||||
import { Issues } from './couchdb-datasources/issues';
|
import { Issues } from './couchdb-datasources/issues';
|
||||||
import { RedmineTypes } from '@app/redmine-types/index';
|
import { RedmineTypes } from '@app/redmine-types/index';
|
||||||
|
import { RedmineUserCacheWriterService } from './user-cache-writer/user-cache-writer.service';
|
||||||
|
|
||||||
@Module({})
|
@Module({})
|
||||||
export class EventEmitterModule implements OnModuleInit {
|
export class EventEmitterModule implements OnModuleInit {
|
||||||
|
|
@ -33,6 +34,7 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
CouchDb,
|
CouchDb,
|
||||||
Users,
|
Users,
|
||||||
Issues,
|
Issues,
|
||||||
|
RedmineUserCacheWriterService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
EventEmitterService,
|
EventEmitterService,
|
||||||
|
|
@ -42,6 +44,7 @@ export class EventEmitterModule implements OnModuleInit {
|
||||||
CouchDb,
|
CouchDb,
|
||||||
Users,
|
Users,
|
||||||
Issues,
|
Issues,
|
||||||
|
RedmineUserCacheWriterService,
|
||||||
],
|
],
|
||||||
controllers: [MainController],
|
controllers: [MainController],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import nano from 'nano';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { Issues } from '../couchdb-datasources/issues';
|
import { Issues } from '../couchdb-datasources/issues';
|
||||||
import { SaveResponse } from '../models/save-response';
|
import { SaveResponse } from '../models/save-response';
|
||||||
|
import { Timestamped } from '../models/timestamped';
|
||||||
|
import { TimestampNowFill } from '../utils/timestamp-now-fill';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RedmineIssuesCacheWriterService {
|
export class RedmineIssuesCacheWriterService {
|
||||||
|
|
@ -23,24 +25,21 @@ export class RedmineIssuesCacheWriterService {
|
||||||
let prevIssue: (nano.DocumentGetResponse & RedmineTypes.Issue) | null;
|
let prevIssue: (nano.DocumentGetResponse & RedmineTypes.Issue) | null;
|
||||||
const issueDb = await Issues.getDatasource();
|
const issueDb = await Issues.getDatasource();
|
||||||
if (!issueDb) {
|
if (!issueDb) {
|
||||||
console.error(`CouchDb datasource must defined`);
|
throw `CouchDb datasource must defined`;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
prevIssue = await issueDb.get(String(id));
|
prevIssue = await issueDb.get(String(id));
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
prevIssue = null;
|
prevIssue = null;
|
||||||
}
|
}
|
||||||
let newIssue: nano.DocumentGetResponse & RedmineTypes.Issue;
|
const newIssue: nano.DocumentGetResponse &
|
||||||
if (!prevIssue) {
|
RedmineTypes.Issue &
|
||||||
newIssue = { ...(issue as any) };
|
Timestamped = TimestampNowFill({ ...(issue as any) });
|
||||||
newIssue._id = String(id);
|
|
||||||
await issueDb.insert(newIssue);
|
|
||||||
} else {
|
|
||||||
newIssue = { ...(issue as any) };
|
|
||||||
newIssue._id = String(id);
|
newIssue._id = String(id);
|
||||||
|
if (prevIssue) {
|
||||||
newIssue._rev = prevIssue._rev;
|
newIssue._rev = prevIssue._rev;
|
||||||
await issueDb.insert(newIssue);
|
|
||||||
}
|
}
|
||||||
|
await issueDb.insert(newIssue);
|
||||||
const res = {
|
const res = {
|
||||||
prev: prevIssue,
|
prev: prevIssue,
|
||||||
current: newIssue,
|
current: newIssue,
|
||||||
|
|
|
||||||
4
libs/event-emitter/src/issues/issues.controller.ts
Normal file
4
libs/event-emitter/src/issues/issues.controller.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { Controller } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Controller('issues')
|
||||||
|
export class IssuesController {}
|
||||||
76
libs/event-emitter/src/issues/issues.service.ts
Normal file
76
libs/event-emitter/src/issues/issues.service.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { RedmineTypes } from '@app/redmine-types/index';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Issues } from '../couchdb-datasources/issues';
|
||||||
|
import { RedmineEventsGateway } from '../events/redmine-events.gateway';
|
||||||
|
import { RedmineIssuesCacheWriterService } from '../issue-cache-writer/redmine-issues-cache-writer.service';
|
||||||
|
import { RedmineDataLoader } from '../redmine-data-loader/redmine-data-loader';
|
||||||
|
import { MemoryCache } from '../utils/memory-cache';
|
||||||
|
|
||||||
|
export const ISSUE_MEMORY_CACHE_LIFETIME = 30 * 1000;
|
||||||
|
const ISSUE_MEMORY_CACHE_AUTOCLEAN_INTERVAL = 1000 * 60 * 5;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IssuesService {
|
||||||
|
private memoryCache = new MemoryCache<number, RedmineTypes.Issue>(
|
||||||
|
ISSUE_MEMORY_CACHE_LIFETIME,
|
||||||
|
ISSUE_MEMORY_CACHE_AUTOCLEAN_INTERVAL,
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private redmineDataLoader: RedmineDataLoader,
|
||||||
|
private issues: Issues,
|
||||||
|
private redmineIssuesCacheWriterService: RedmineIssuesCacheWriterService,
|
||||||
|
private redmineEventsGateway: RedmineEventsGateway,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getIssue(
|
||||||
|
issueId: number,
|
||||||
|
force = false,
|
||||||
|
): Promise<RedmineTypes.Issue | null> {
|
||||||
|
const issueFromMemoryCache = this.getIssueFromMemoryCache(issueId);
|
||||||
|
if (issueFromMemoryCache) {
|
||||||
|
return issueFromMemoryCache;
|
||||||
|
}
|
||||||
|
const issueFromCache = await this.getIssueFromCache(issueId);
|
||||||
|
if (issueFromCache) {
|
||||||
|
this.memoryCache.set(issueId, issueFromCache);
|
||||||
|
return issueFromCache;
|
||||||
|
}
|
||||||
|
if (force) {
|
||||||
|
// force = true - прямо из redmine
|
||||||
|
const issueFromRedmine = await this.redmineDataLoader.loadIssue(issueId);
|
||||||
|
if (issueFromRedmine) {
|
||||||
|
await this.redmineIssuesCacheWriterService.saveIssue(issueFromRedmine);
|
||||||
|
this.memoryCache.set(issueId, issueFromRedmine);
|
||||||
|
return issueFromRedmine;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// force = false - через очередь
|
||||||
|
this.redmineEventsGateway.addIssues([issueId]);
|
||||||
|
const unknownIssue = { ...RedmineTypes.Unknown.issue };
|
||||||
|
this.memoryCache.set(issueId, unknownIssue);
|
||||||
|
return unknownIssue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getIssueFromMemoryCache(issueId: number): RedmineTypes.Issue | null {
|
||||||
|
return this.memoryCache.get(issueId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIssueFromRedmine(
|
||||||
|
issueId: number,
|
||||||
|
): Promise<RedmineTypes.Issue | null> {
|
||||||
|
return await this.redmineDataLoader.loadIssue(issueId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIssueFromCache(issueId: number): Promise<RedmineTypes.Issue | null> {
|
||||||
|
const issueDb = await this.issues.getDatasource();
|
||||||
|
try {
|
||||||
|
return (await issueDb.get(String(issueId))) as any;
|
||||||
|
} catch (ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
export type Timestamped = {
|
export type Timestamped = {
|
||||||
_timestamp: number;
|
timestamp__: number;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { RedmineTypes } from '@app/redmine-types/index';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Users } from '../couchdb-datasources/users';
|
||||||
|
import nano from 'nano';
|
||||||
|
import { Timestamped } from '../models/timestamped';
|
||||||
|
import { TimestampNowFill } from '../utils/timestamp-now-fill';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RedmineUserCacheWriterService {
|
||||||
|
private logger = new Logger(RedmineUserCacheWriterService.name);
|
||||||
|
|
||||||
|
constructor(private users: Users) {}
|
||||||
|
|
||||||
|
async saveUser(
|
||||||
|
user: RedmineTypes.User,
|
||||||
|
): Promise<RedmineTypes.User & Timestamped> {
|
||||||
|
this.logger.debug(`Saving user ${user.id} - ${user.login}`);
|
||||||
|
const id = user.id;
|
||||||
|
const userDb = await this.users.getDatasource();
|
||||||
|
let prevUser: (nano.DocumentGetResponse & RedmineTypes.User) | null;
|
||||||
|
try {
|
||||||
|
prevUser = await userDb.get(String(id));
|
||||||
|
} catch (ex) {
|
||||||
|
prevUser = null;
|
||||||
|
}
|
||||||
|
const newUser: nano.DocumentGetResponse & RedmineTypes.User & Timestamped =
|
||||||
|
TimestampNowFill({ ...(user as any) });
|
||||||
|
if (prevUser) {
|
||||||
|
newUser._rev = prevUser._rev;
|
||||||
|
}
|
||||||
|
await userDb.insert(newUser);
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
libs/event-emitter/src/users/users.controller.ts
Normal file
23
libs/event-emitter/src/users/users.controller.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { RedmineTypes } from '@app/redmine-types/index';
|
||||||
|
import { Controller, Get, Param } from '@nestjs/common';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
|
@Controller('users')
|
||||||
|
export class UsersController {
|
||||||
|
constructor(private readonly usersService: UsersService) {}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
async getUser(@Param('id') id: number): Promise<RedmineTypes.User> {
|
||||||
|
return await this.usersService.getUser(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id.json')
|
||||||
|
async getUserLikeRedmine(
|
||||||
|
@Param('id') id: number,
|
||||||
|
): Promise<{ user: RedmineTypes.User }> {
|
||||||
|
const user = await this.usersService.getUser(id);
|
||||||
|
return {
|
||||||
|
user: user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
70
libs/event-emitter/src/users/users.service.ts
Normal file
70
libs/event-emitter/src/users/users.service.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { RedmineTypes } from '@app/redmine-types/index';
|
||||||
|
import { Timestamped } from '../models/timestamped';
|
||||||
|
import { Users } from '../couchdb-datasources/users';
|
||||||
|
import { RedmineDataLoader } from '../redmine-data-loader/redmine-data-loader';
|
||||||
|
import { TimestampNowFill } from '../utils/timestamp-now-fill';
|
||||||
|
import { RedmineUserCacheWriterService } from '../user-cache-writer/user-cache-writer.service';
|
||||||
|
import { TimestampIsTimeouted } from '../utils/timestamp-is-timeouted';
|
||||||
|
|
||||||
|
export const USER_MEMORY_CACHE_LIFETIME = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersService {
|
||||||
|
private memoryCache: Record<number, RedmineTypes.User & Timestamped> = {};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private users: Users,
|
||||||
|
private redmineDataLoader: RedmineDataLoader,
|
||||||
|
private redmineUserCacheWriterService: RedmineUserCacheWriterService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getUser(userId: number): Promise<RedmineTypes.User> {
|
||||||
|
const userFromMemoryCache = this.getUserFromMemoryCache(userId);
|
||||||
|
if (userFromMemoryCache) {
|
||||||
|
return userFromMemoryCache;
|
||||||
|
}
|
||||||
|
const userFromCache = await this.getUserFromCache(userId);
|
||||||
|
if (userFromCache) {
|
||||||
|
this.memoryCache[userId] = TimestampNowFill({ ...userFromCache });
|
||||||
|
return this.memoryCache[userId];
|
||||||
|
}
|
||||||
|
let userFromRedmine = await this.getUserFromRedmine(userId);
|
||||||
|
if (userFromRedmine) {
|
||||||
|
userFromRedmine = await this.redmineUserCacheWriterService.saveUser(
|
||||||
|
userFromRedmine,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const unknownUser = TimestampNowFill({ ...RedmineTypes.Unknown.user });
|
||||||
|
this.memoryCache[userId] = (userFromRedmine || unknownUser) as any;
|
||||||
|
return this.memoryCache[userId];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserFromRedmine(userId: number): Promise<RedmineTypes.User | null> {
|
||||||
|
return await this.redmineDataLoader.loadUser(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserFromCache(
|
||||||
|
userId: number,
|
||||||
|
): Promise<(RedmineTypes.User & Timestamped) | null> {
|
||||||
|
const usersDb = await this.users.getDatasource();
|
||||||
|
try {
|
||||||
|
return (await usersDb.get(String(userId))) as any;
|
||||||
|
} catch (ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserFromMemoryCache(userId: number): RedmineTypes.User | null {
|
||||||
|
if (
|
||||||
|
this.memoryCache[userId] &&
|
||||||
|
!TimestampIsTimeouted(
|
||||||
|
this.memoryCache[userId],
|
||||||
|
USER_MEMORY_CACHE_LIFETIME,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return this.memoryCache[userId];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
libs/event-emitter/src/utils/memory-cache.ts
Normal file
48
libs/event-emitter/src/utils/memory-cache.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Timestamped } from '../models/timestamped';
|
||||||
|
import { TimestampIsTimeouted } from './timestamp-is-timeouted';
|
||||||
|
import { TimestampNowFill } from './timestamp-now-fill';
|
||||||
|
|
||||||
|
export class MemoryCache<K, T> {
|
||||||
|
private memoryCache = {};
|
||||||
|
|
||||||
|
constructor(private timeout: number, private autoclean: number = 0) {
|
||||||
|
if (autoclean > 0) {
|
||||||
|
this.startAutoclean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: K): T | null {
|
||||||
|
const k = key as any;
|
||||||
|
if (this.memoryCache[k]) {
|
||||||
|
if (TimestampIsTimeouted(this.memoryCache[k], this.timeout)) {
|
||||||
|
delete this.memoryCache[k];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.memoryCache[k];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: K, value: T): T & Timestamped {
|
||||||
|
this.memoryCache[key as any] = TimestampNowFill({ ...value });
|
||||||
|
return this.memoryCache[key as any];
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanTimeouted(): void {
|
||||||
|
for (const key in this.memoryCache) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(this.memoryCache, key)) {
|
||||||
|
const item = this.memoryCache[key];
|
||||||
|
if (TimestampIsTimeouted(item, this.timeout)) {
|
||||||
|
delete this.memoryCache[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startAutoclean() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.cleanTimeouted();
|
||||||
|
this.startAutoclean();
|
||||||
|
}, this.autoclean);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
libs/event-emitter/src/utils/timestamp-is-timeouted.ts
Normal file
9
libs/event-emitter/src/utils/timestamp-is-timeouted.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Timestamped } from '../models/timestamped';
|
||||||
|
|
||||||
|
export function TimestampIsTimeouted(
|
||||||
|
obj: Timestamped,
|
||||||
|
timeout: number,
|
||||||
|
): boolean {
|
||||||
|
const now = new Date().getDate();
|
||||||
|
return obj.timestamp__ < now - timeout;
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,5 @@ import { Timestamped } from '../models/timestamped';
|
||||||
|
|
||||||
export function TimestampNowFill<T>(obj: T): T & Timestamped {
|
export function TimestampNowFill<T>(obj: T): T & Timestamped {
|
||||||
const now = new Date().getDate();
|
const now = new Date().getDate();
|
||||||
const res: any = obj;
|
return { ...obj, timestamp__: now };
|
||||||
res._timestamp = now;
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './redmine-data-loader.module';
|
|
||||||
export * from './redmine-data-loader.service';
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { RedmineTypes } from '@app/redmine-types/index';
|
|
||||||
import { DynamicModule, Module } from '@nestjs/common';
|
|
||||||
import nano from 'nano';
|
|
||||||
import { RedmineDataLoaderService } from './redmine-data-loader.service';
|
|
||||||
|
|
||||||
@Module({})
|
|
||||||
export class RedmineDataLoaderModule {
|
|
||||||
static register(params: {
|
|
||||||
issueDocumentScopeProvider: () => Promise<
|
|
||||||
nano.DocumentScope<RedmineTypes.Issue>
|
|
||||||
>;
|
|
||||||
userDocumentScopeProvider: () => Promise<
|
|
||||||
nano.DocumentScope<RedmineTypes.User>
|
|
||||||
>;
|
|
||||||
}): DynamicModule {
|
|
||||||
return {
|
|
||||||
module: RedmineDataLoaderModule,
|
|
||||||
providers: [
|
|
||||||
RedmineDataLoaderService,
|
|
||||||
{
|
|
||||||
provide: 'ISSUE_DOCUMENT_SCOPE',
|
|
||||||
useValue: params.issueDocumentScopeProvider,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: 'USER_DOCUMENT_SCOPE',
|
|
||||||
useValue: params.userDocumentScopeProvider,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
exports: [RedmineDataLoaderService],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RedmineDataLoaderService {}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"declaration": true,
|
|
||||||
"outDir": "../../dist/libs/redmine-data-loader"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export * from './redmine-issues-cache-writer.module';
|
|
||||||
export * from './redmine-issues-cache-writer.service';
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { DynamicModule, Module } from '@nestjs/common';
|
|
||||||
import { RedmineIssuesCacheWriterService } from './redmine-issues-cache-writer.service';
|
|
||||||
import nano = require('nano');
|
|
||||||
import { RedmineTypes } from 'libs/redmine-types';
|
|
||||||
|
|
||||||
@Module({})
|
|
||||||
export class RedmineIssuesCacheWriterModule {
|
|
||||||
static register(params: {
|
|
||||||
issueDocumentScopeProvider: () => Promise<
|
|
||||||
nano.DocumentScope<RedmineTypes.Issue>
|
|
||||||
>;
|
|
||||||
}): DynamicModule {
|
|
||||||
return {
|
|
||||||
module: RedmineIssuesCacheWriterModule,
|
|
||||||
providers: [
|
|
||||||
RedmineIssuesCacheWriterService,
|
|
||||||
{
|
|
||||||
provide: 'ISSUE_DOCUMENT_SCOPE',
|
|
||||||
useValue: params.issueDocumentScopeProvider,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
exports: [RedmineIssuesCacheWriterService],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { RedmineTypes } from 'libs/redmine-types';
|
|
||||||
import nano from 'nano';
|
|
||||||
import { Subject } from 'rxjs';
|
|
||||||
import { SaveResponse } from './save-response';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RedmineIssuesCacheWriterService {
|
|
||||||
private logger = new Logger(RedmineIssuesCacheWriterService.name);
|
|
||||||
|
|
||||||
subject = new Subject<SaveResponse>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject('ISSUE_DOCUMENT_SCOPE')
|
|
||||||
private issueDbProvider: () => Promise<
|
|
||||||
nano.DocumentScope<RedmineTypes.Issue>
|
|
||||||
>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async saveIssue(issue: RedmineTypes.Issue): Promise<SaveResponse> {
|
|
||||||
this.logger.debug(
|
|
||||||
`Saving issue ${issue?.id || '-'} - ${
|
|
||||||
issue?.subject || '-'
|
|
||||||
}, issue data = ${JSON.stringify(issue)}`,
|
|
||||||
);
|
|
||||||
const id = Number(issue['id']);
|
|
||||||
let prevIssue: (nano.DocumentGetResponse & RedmineTypes.Issue) | null;
|
|
||||||
const issueDb = await this.issueDbProvider();
|
|
||||||
try {
|
|
||||||
prevIssue = await issueDb.get(String(id));
|
|
||||||
} catch (ex) {
|
|
||||||
prevIssue = null;
|
|
||||||
}
|
|
||||||
let newIssue: nano.DocumentGetResponse & RedmineTypes.Issue;
|
|
||||||
if (!prevIssue) {
|
|
||||||
newIssue = { ...(issue as any) };
|
|
||||||
newIssue._id = String(id);
|
|
||||||
await issueDb.insert(newIssue);
|
|
||||||
} else {
|
|
||||||
newIssue = { ...(issue as any) };
|
|
||||||
newIssue._id = String(id);
|
|
||||||
newIssue._rev = prevIssue._rev;
|
|
||||||
await issueDb.insert(newIssue);
|
|
||||||
}
|
|
||||||
const res = {
|
|
||||||
prev: prevIssue,
|
|
||||||
current: newIssue,
|
|
||||||
journalsDiff: this.getJournalsDiff(prevIssue, newIssue),
|
|
||||||
};
|
|
||||||
this.logger.debug(
|
|
||||||
`Saving issue success ${issue?.id || '-'} - ${issue?.subject || '-'}`,
|
|
||||||
);
|
|
||||||
this.subject.next(res);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
getJournalsDiff(
|
|
||||||
prev: (nano.DocumentGetResponse & RedmineTypes.Issue) | null,
|
|
||||||
current: nano.DocumentGetResponse & RedmineTypes.Issue,
|
|
||||||
): RedmineTypes.Journal[] {
|
|
||||||
if (
|
|
||||||
(!prev || !prev.journals || prev.journals.length === 0) &&
|
|
||||||
current?.journals
|
|
||||||
) {
|
|
||||||
return current.journals;
|
|
||||||
} else if (prev?.journals && current?.journals) {
|
|
||||||
return this.calcJournalsDiff(prev?.journals, current?.journals);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private calcJournalsDiff(
|
|
||||||
prev: RedmineTypes.Journal[],
|
|
||||||
current: RedmineTypes.Journal[],
|
|
||||||
): RedmineTypes.Journal[] {
|
|
||||||
const res: RedmineTypes.Journal[] = [];
|
|
||||||
|
|
||||||
const prevIds = prev.map((item) => item.id);
|
|
||||||
|
|
||||||
for (let i = 0; i < current.length; i++) {
|
|
||||||
const currentItem = current[i];
|
|
||||||
if (!prevIds.includes(currentItem.id)) {
|
|
||||||
res.push(currentItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import { RedmineTypes } from 'libs/redmine-types';
|
|
||||||
|
|
||||||
export type SaveResponse = {
|
|
||||||
prev: RedmineTypes.Issue | null;
|
|
||||||
current: RedmineTypes.Issue;
|
|
||||||
journalsDiff: RedmineTypes.Journal[];
|
|
||||||
};
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"declaration": true,
|
|
||||||
"outDir": "../../dist/libs/redmine-issues-cache-writer"
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
|
|
||||||
}
|
|
||||||
|
|
@ -81,9 +81,7 @@
|
||||||
"<rootDir>/libs/"
|
"<rootDir>/libs/"
|
||||||
],
|
],
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"^@app/event-emitter(|/.*)$": "<rootDir>/libs/event-emitter/src/$1",
|
"^@app/event-emitter(|/.*)$": "<rootDir>/libs/event-emitter/src/$1"
|
||||||
"^@app/redmine-issues-cache-writer(|/.*)$": "<rootDir>/libs/redmine-issues-cache-writer/src/$1",
|
|
||||||
"^@app/redmine-data-loader(|/.*)$": "<rootDir>/libs/redmine-data-loader/src/$1"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
import { EventEmitterModule } from '@app/event-emitter';
|
import { EventEmitterModule } from '@app/event-emitter';
|
||||||
import { MainController } from '@app/event-emitter/main/main.controller';
|
import { MainController } from '@app/event-emitter/main/main.controller';
|
||||||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { Issues } from './datasources/issues';
|
|
||||||
import configuration from './configs/app';
|
import configuration from './configs/app';
|
||||||
import { RedmineEventsGateway } from '@app/event-emitter/events/redmine-events.gateway';
|
|
||||||
import { CouchDb } from './datasources/couchdb';
|
|
||||||
import { Users } from './datasources/users';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -18,40 +14,6 @@ import { Users } from './datasources/users';
|
||||||
ConfigModule.forRoot({ load: [configuration] }),
|
ConfigModule.forRoot({ load: [configuration] }),
|
||||||
],
|
],
|
||||||
controllers: [AppController, MainController],
|
controllers: [AppController, MainController],
|
||||||
providers: [AppService, Issues, CouchDb, Users],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
export class AppModule implements OnModuleInit {
|
export class AppModule {}
|
||||||
private logger = new Logger(AppModule.name);
|
|
||||||
|
|
||||||
constructor(private redmineEventsGateway: RedmineEventsGateway) {}
|
|
||||||
|
|
||||||
onModuleInit() {
|
|
||||||
// const queue = this.redmineEventsGateway.getIssuesChangesQueue();
|
|
||||||
// const subj = queue.queue;
|
|
||||||
// subj.subscribe(async (issues: any) => {
|
|
||||||
// this.logger.debug(`Changed issues = ${JSON.stringify(issues)}`);
|
|
||||||
//
|
|
||||||
// for (let i = 0; i < issues.length; i++) {
|
|
||||||
// const issue: RedmineTypes.Issue = issues[i];
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// this.logger.debug(
|
|
||||||
// `Save issue #${issue.id} - ${JSON.stringify(issue)}`,
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// const response = await this.redmineIssuesCacheWriterService.saveIssue(
|
|
||||||
// issue,
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// this.logger.debug(
|
|
||||||
// `Save issue #${issue.id} response = ${JSON.stringify(response)}`,
|
|
||||||
// );
|
|
||||||
// } catch (ex) {
|
|
||||||
// this.logger.error(`Saving issue error - ${ex}`, null, {
|
|
||||||
// issue: issue,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import * as nano from 'nano';
|
|
||||||
import configuration from '../configs/app';
|
|
||||||
|
|
||||||
const config = configuration();
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class CouchDb {
|
|
||||||
private static logger = new Logger(CouchDb.name);
|
|
||||||
private static couchdb: nano.ServerScope | null = null;
|
|
||||||
|
|
||||||
static getCouchDb(): nano.ServerScope {
|
|
||||||
if (CouchDb.couchdb) {
|
|
||||||
return CouchDb.couchdb;
|
|
||||||
}
|
|
||||||
const n = nano(config.couchDbUrl);
|
|
||||||
CouchDb.logger.log(`CouchDb connected by url ${config.couchDbUrl} ...`);
|
|
||||||
CouchDb.couchdb = n;
|
|
||||||
return CouchDb.couchdb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { RedmineTypes } from '@app/redmine-types/index';
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import configuration from '../configs/app';
|
|
||||||
import nano = require('nano');
|
|
||||||
import { CouchDb } from './couchdb';
|
|
||||||
|
|
||||||
const config = configuration();
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class Issues {
|
|
||||||
private static logger = new Logger(Issues.name);
|
|
||||||
private static issuesDb = null;
|
|
||||||
|
|
||||||
static async getDatasource(): Promise<
|
|
||||||
nano.DocumentScope<RedmineTypes.Issue>
|
|
||||||
> {
|
|
||||||
if (Issues.issuesDb) {
|
|
||||||
return Issues.issuesDb;
|
|
||||||
}
|
|
||||||
const n = CouchDb.getCouchDb();
|
|
||||||
const dbs = await n.db.list();
|
|
||||||
if (!dbs.includes(config.dbs.issues)) {
|
|
||||||
await n.db.create(config.dbs.issues);
|
|
||||||
}
|
|
||||||
Issues.issuesDb = await n.db.use(config.dbs.issues);
|
|
||||||
Issues.logger.log(`Connected to issues db - ${config.dbs.issues}`);
|
|
||||||
return Issues.issuesDb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { RedmineTypes } from '@app/redmine-types/index';
|
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import nano from 'nano';
|
|
||||||
import { CouchDb } from './couchdb';
|
|
||||||
import configuration from '../configs/app';
|
|
||||||
|
|
||||||
const config = configuration();
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class Users {
|
|
||||||
private static logger = new Logger(Users.name);
|
|
||||||
private static usersDb = null;
|
|
||||||
|
|
||||||
static async getDatasource(): Promise<nano.DocumentScope<RedmineTypes.User>> {
|
|
||||||
if (Users.usersDb) {
|
|
||||||
return Users.usersDb;
|
|
||||||
}
|
|
||||||
const n = CouchDb.getCouchDb();
|
|
||||||
const dbs = await n.db.list();
|
|
||||||
if (!dbs.includes(config.dbs.users)) {
|
|
||||||
await n.db.create(config.dbs.users);
|
|
||||||
}
|
|
||||||
Users.usersDb = await n.db.use(config.dbs.users);
|
|
||||||
Users.logger.log(`Connected to users db - ${config.dbs.users}`);
|
|
||||||
return Users.usersDb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -24,20 +24,8 @@
|
||||||
"@app/event-emitter/*": [
|
"@app/event-emitter/*": [
|
||||||
"libs/event-emitter/src/*"
|
"libs/event-emitter/src/*"
|
||||||
],
|
],
|
||||||
"@app/redmine-issues-cache-writer": [
|
|
||||||
"libs/redmine-issues-cache-writer/src"
|
|
||||||
],
|
|
||||||
"@app/redmine-issues-cache-writer/*": [
|
|
||||||
"libs/redmine-issues-cache-writer/src/*"
|
|
||||||
],
|
|
||||||
"@app/redmine-types/*": [
|
"@app/redmine-types/*": [
|
||||||
"libs/redmine-types/*"
|
"libs/redmine-types/*"
|
||||||
],
|
|
||||||
"@app/redmine-data-loader": [
|
|
||||||
"libs/redmine-data-loader/src"
|
|
||||||
],
|
|
||||||
"@app/redmine-data-loader/*": [
|
|
||||||
"libs/redmine-data-loader/src/*"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue