Добавлена поддержка работы с юзерами

This commit is contained in:
Pavel Gnedov 2022-07-19 03:44:08 +07:00
parent 60b0bdbcd4
commit d20b05d760
17 changed files with 54 additions and 170 deletions

View file

@ -1,5 +1,5 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { RedmineTypes } from 'libs/redmine-types'; import { RedmineTypes } from '../models/redmine-types';
import nano from 'nano'; import nano from 'nano';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { Issues } from '../couchdb-datasources/issues'; import { Issues } from '../couchdb-datasources/issues';

View file

@ -1,6 +1,4 @@
/// <reference types="typescript" /> // eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace
// eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword
export module RedmineTypes { export module RedmineTypes {
export type IdAndName = { export type IdAndName = {
id: number; id: number;
@ -51,7 +49,7 @@ export module RedmineTypes {
journals?: Journal[]; journals?: Journal[];
}; };
// eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword // eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword, @typescript-eslint/no-namespace
export module Unknown { export module Unknown {
export const num = -1; export const num = -1;
export const str = ''; export const str = '';
@ -83,6 +81,7 @@ export module RedmineTypes {
export const user: User = { export const user: User = {
id: num, id: num,
login: unknownName,
firstname: unknownName, firstname: unknownName,
lastname: unknownName, lastname: unknownName,
mail: str, mail: str,
@ -96,4 +95,14 @@ export module RedmineTypes {
lastname: string; lastname: string;
mail: string; mail: string;
}; };
export function ExtractUser(obj: User): User {
return {
id: obj.id,
login: obj.login,
firstname: obj.firstname,
lastname: obj.lastname,
mail: obj.mail,
};
}
} }

View file

@ -1,12 +0,0 @@
{
"name": "redmine-types",
"version": "0.1.0",
"description": "",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Pavel Gnedov",
"license": "MIT"
}

View file

@ -1,4 +1,4 @@
import { RedmineTypes } from 'libs/redmine-types'; import { RedmineTypes } from './redmine-types';
// TODO: Переименовать в IssueCacheSaveResponse // TODO: Переименовать в IssueCacheSaveResponse

View file

@ -51,7 +51,7 @@ export class RedmineDataLoader {
this.logger.debug( this.logger.debug(
`Loaded user, userNumber = ${userNumber}, login = ${user.login}, firstname = ${user.firstname}, lastname = ${user.lastname}`, `Loaded user, userNumber = ${userNumber}, login = ${user.login}, firstname = ${user.firstname}, lastname = ${user.lastname}`,
); );
return user; return RedmineTypes.ExtractUser(user);
} }
private getIssueUrl(issueNumber: number): string { private getIssueUrl(issueNumber: number): string {

View file

@ -24,7 +24,7 @@ export class RedmineUserCacheWriterService {
prevUser = null; prevUser = null;
} }
const newUser: nano.DocumentGetResponse & RedmineTypes.User & Timestamped = const newUser: nano.DocumentGetResponse & RedmineTypes.User & Timestamped =
TimestampNowFill({ ...(user as any) }); TimestampNowFill({ ...(user as any), _id: String(id) });
if (prevUser) { if (prevUser) {
newUser._rev = prevUser._rev; newUser._rev = prevUser._rev;
} }

View file

@ -11,7 +11,7 @@ export class UsersController {
return await this.usersService.getUser(id); return await this.usersService.getUser(id);
} }
@Get(':id.json') @Get(':id/json')
async getUserLikeRedmine( async getUserLikeRedmine(
@Param('id') id: number, @Param('id') id: number,
): Promise<{ user: RedmineTypes.User }> { ): Promise<{ user: RedmineTypes.User }> {

View file

@ -1,17 +1,21 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { RedmineTypes } from '../models/redmine-types';
import { Timestamped } from '../models/timestamped'; import { Timestamped } from '../models/timestamped';
import { Users } from '../couchdb-datasources/users'; import { Users } from '../couchdb-datasources/users';
import { RedmineDataLoader } from '../redmine-data-loader/redmine-data-loader'; 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 { RedmineUserCacheWriterService } from '../user-cache-writer/user-cache-writer.service';
import { TimestampIsTimeouted } from '../utils/timestamp-is-timeouted'; import { RedmineTypes } from '../models/redmine-types';
import { MemoryCache } from '../utils/memory-cache';
export const USER_MEMORY_CACHE_LIFETIME = 24 * 60 * 60 * 1000; export const USER_MEMORY_CACHE_LIFETIME = 24 * 60 * 60 * 1000;
const USER_MEMORY_CACHE_AUTOCLEAN_INTERVAL = 1000 * 60 * 5;
@Injectable() @Injectable()
export class UsersService { export class UsersService {
private memoryCache: Record<number, RedmineTypes.User & Timestamped> = {}; private logger = new Logger(UsersService.name);
private memoryCache = new MemoryCache<number, RedmineTypes.User>(
USER_MEMORY_CACHE_LIFETIME,
USER_MEMORY_CACHE_AUTOCLEAN_INTERVAL,
);
constructor( constructor(
private users: Users, private users: Users,
@ -26,8 +30,8 @@ export class UsersService {
} }
const userFromCache = await this.getUserFromCache(userId); const userFromCache = await this.getUserFromCache(userId);
if (userFromCache) { if (userFromCache) {
this.memoryCache[userId] = TimestampNowFill({ ...userFromCache }); this.memoryCache.set(userId, userFromCache);
return this.memoryCache[userId]; return userFromCache;
} }
let userFromRedmine = await this.getUserFromRedmine(userId); let userFromRedmine = await this.getUserFromRedmine(userId);
if (userFromRedmine) { if (userFromRedmine) {
@ -35,13 +39,18 @@ export class UsersService {
userFromRedmine, userFromRedmine,
); );
} }
const unknownUser = TimestampNowFill({ ...RedmineTypes.Unknown.user }); return this.memoryCache.set(
this.memoryCache[userId] = (userFromRedmine || unknownUser) as any; userId,
return this.memoryCache[userId]; userFromRedmine || RedmineTypes.Unknown.user,
);
} }
async getUserFromRedmine(userId: number): Promise<RedmineTypes.User | null> { async getUserFromRedmine(userId: number): Promise<RedmineTypes.User | null> {
return await this.redmineDataLoader.loadUser(userId); const user = await this.redmineDataLoader.loadUser(userId);
this.logger.debug(
`Get user from redmine with userId = ${userId}, login = ${user.login}`,
);
return user;
} }
async getUserFromCache( async getUserFromCache(
@ -49,22 +58,23 @@ export class UsersService {
): Promise<(RedmineTypes.User & Timestamped) | null> { ): Promise<(RedmineTypes.User & Timestamped) | null> {
const usersDb = await this.users.getDatasource(); const usersDb = await this.users.getDatasource();
try { try {
return (await usersDb.get(String(userId))) as any; const user = (await usersDb.get(String(userId))) as any;
this.logger.debug(
`Get user from couchdb with userId = ${userId}, login = ${user.login}`,
);
return user;
} catch (ex) { } catch (ex) {
return null; return null;
} }
} }
getUserFromMemoryCache(userId: number): RedmineTypes.User | null { getUserFromMemoryCache(userId: number): RedmineTypes.User | null {
if ( const user = this.memoryCache.get(userId);
this.memoryCache[userId] && if (user) {
!TimestampIsTimeouted( this.logger.debug(
this.memoryCache[userId], `Get user from memory cache with userId = ${userId}, login = ${user.login}`,
USER_MEMORY_CACHE_LIFETIME, );
)
) {
return this.memoryCache[userId];
} }
return null; return user;
} }
} }

View file

@ -11,7 +11,7 @@ export class MemoryCache<K, T> {
} }
} }
get(key: K): T | null { get(key: K): (T & Timestamped) | null {
const k = key as any; const k = key as any;
if (this.memoryCache[k]) { if (this.memoryCache[k]) {
if (TimestampIsTimeouted(this.memoryCache[k], this.timeout)) { if (TimestampIsTimeouted(this.memoryCache[k], this.timeout)) {

View file

@ -4,6 +4,6 @@ export function TimestampIsTimeouted(
obj: Timestamped, obj: Timestamped,
timeout: number, timeout: number,
): boolean { ): boolean {
const now = new Date().getDate(); const now = new Date().getTime();
return obj.timestamp__ < now - timeout; return obj.timestamp__ < now - timeout;
} }

View file

@ -1,6 +1,6 @@
import { Timestamped } from '../models/timestamped'; 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().getTime();
return { ...obj, timestamp__: now }; return { ...obj, timestamp__: now };
} }

View file

@ -1,99 +0,0 @@
/// <reference types="typescript" />
// eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword
export module RedmineTypes {
export type IdAndName = {
id: number;
name: string;
};
export type CustomField = {
id: number;
name: string;
value: string;
};
export type JournalDetail = {
property: string;
name: string;
old_value?: string;
new_value?: string;
};
export type Journal = {
id: number;
user: IdAndName;
notes?: string;
created_on: string;
details?: JournalDetail[];
};
export type Issue = {
id: number;
project: IdAndName;
tracker: IdAndName;
status: IdAndName;
priority: IdAndName;
author: IdAndName;
category: IdAndName;
fixed_version: IdAndName;
subject: string;
description: string;
start_date: string;
done_ratio: number;
spent_hours: number;
total_spent_hours: number;
custom_fields: CustomField[];
created_on: string;
updated_on?: string;
closed_on?: string;
relations?: Record<string, any>[];
journals?: Journal[];
};
// eslint-disable-next-line @typescript-eslint/prefer-namespace-keyword
export module Unknown {
export const num = -1;
export const str = '';
export const idAndName: IdAndName = {
id: -1,
name: str,
};
export const unknownName = 'Unknown';
export const subject = 'Unknown';
export const date = '1970-01-01T00:00:00Z';
export const issue: Issue = {
id: num,
project: idAndName,
tracker: idAndName,
status: idAndName,
priority: idAndName,
author: idAndName,
category: idAndName,
fixed_version: idAndName,
subject: subject,
description: str,
start_date: date,
done_ratio: num,
spent_hours: num,
total_spent_hours: num,
custom_fields: [],
created_on: date,
};
export const user: User = {
id: num,
firstname: unknownName,
lastname: unknownName,
mail: str,
};
}
export type User = {
id: number;
login: string;
firstname: string;
lastname: string;
mail: string;
};
}

View file

@ -1,12 +0,0 @@
{
"name": "redmine-types",
"version": "0.1.0",
"description": "",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Pavel Gnedov",
"license": "MIT"
}

11
package-lock.json generated
View file

@ -8,9 +8,6 @@
"name": "eltex-redmine-helper-2", "name": "eltex-redmine-helper-2",
"version": "0.0.1", "version": "0.0.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"workspaces": [
"libs/redmine-types"
],
"dependencies": { "dependencies": {
"@nestjs/common": "^8.0.0", "@nestjs/common": "^8.0.0",
"@nestjs/config": "^2.0.0", "@nestjs/config": "^2.0.0",
@ -54,6 +51,7 @@
}, },
"libs/redmine-types": { "libs/redmine-types": {
"version": "0.1.0", "version": "0.1.0",
"extraneous": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@ -6595,10 +6593,6 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/redmine-types": {
"resolved": "libs/redmine-types",
"link": true
},
"node_modules/reflect-metadata": { "node_modules/reflect-metadata": {
"version": "0.1.13", "version": "0.1.13",
"license": "Apache-2.0" "license": "Apache-2.0"
@ -12384,9 +12378,6 @@
"resolve": "^1.1.6" "resolve": "^1.1.6"
} }
}, },
"redmine-types": {
"version": "file:libs/redmine-types"
},
"reflect-metadata": { "reflect-metadata": {
"version": "0.1.13" "version": "0.1.13"
}, },

View file

@ -83,8 +83,5 @@
"moduleNameMapper": { "moduleNameMapper": {
"^@app/event-emitter(|/.*)$": "<rootDir>/libs/event-emitter/src/$1" "^@app/event-emitter(|/.*)$": "<rootDir>/libs/event-emitter/src/$1"
} }
}, }
"workspaces": [
"libs/redmine-types"
]
} }