Исправлены ошибки сборки проекта

This commit is contained in:
Pavel Gnedov 2022-07-18 19:22:49 +07:00
parent 326b97931c
commit eb16e92a6c
27 changed files with 284 additions and 327 deletions

4
.vscode/launch.json vendored
View file

@ -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}"
} }

View file

@ -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],
}; };

View file

@ -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); newIssue._id = String(id);
await issueDb.insert(newIssue); if (prevIssue) {
} else {
newIssue = { ...(issue as any) };
newIssue._id = String(id);
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,

View file

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('issues')
export class IssuesController {}

View 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;
}
}
}

View file

@ -1,3 +1,3 @@
export type Timestamped = { export type Timestamped = {
_timestamp: number; timestamp__: number;
}; };

View file

@ -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;
}
}

View 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,
};
}
}

View 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;
}
}

View 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);
}
}

View 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;
}

View file

@ -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;
} }

View file

@ -1,2 +0,0 @@
export * from './redmine-data-loader.module';
export * from './redmine-data-loader.service';

View file

@ -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],
};
}
}

View file

@ -1,4 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class RedmineDataLoaderService {}

View file

@ -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"]
}

View file

@ -1,2 +0,0 @@
export * from './redmine-issues-cache-writer.module';
export * from './redmine-issues-cache-writer.service';

View file

@ -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],
};
}
}

View file

@ -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;
}
}

View file

@ -1,7 +0,0 @@
import { RedmineTypes } from 'libs/redmine-types';
export type SaveResponse = {
prev: RedmineTypes.Issue | null;
current: RedmineTypes.Issue;
journalsDiff: RedmineTypes.Journal[];
};

View file

@ -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"]
}

View file

@ -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": [

View file

@ -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,
// });
// }
// }
// });
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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/*"
] ]
} }
} }