perf(server): optimize performance of APIs (#3079)
* perf: add perf_hooks * perf: add cache * fix: test * feat: support bloom filter * feat: caching token black list * perf: caching i18n instance * fix: test * fix: test * chore: remove prePerfHooks on app * chore: improve i18n instances cache * chore: remove performance measure * fix: package.json * perf: optimize cache strategy * fix: test * fix: bug * test: storer of auth-manager * fix: afterDestroy hook when updating null value * fix: version * chore: fix bug and add test * fix: test * fix: test
This commit is contained in:
parent
2c37910894
commit
474b09c7f2
@ -76,7 +76,10 @@ const app = new Koa();
|
|||||||
|
|
||||||
app.use(async (ctx, next) => {
|
app.use(async (ctx, next) => {
|
||||||
const repository = db.getRepository('users');
|
const repository = db.getRepository('users');
|
||||||
ctx.body = await repository.find();
|
ctx.body = await repository.findAndCount({
|
||||||
|
limit: 20,
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -35,7 +35,10 @@ const User = sequelize.define(
|
|||||||
const app = new Koa();
|
const app = new Koa();
|
||||||
|
|
||||||
app.use(async (ctx, next) => {
|
app.use(async (ctx, next) => {
|
||||||
ctx.body = await User.findAll();
|
ctx.body = await User.findAndCountAll({
|
||||||
|
offset: 0,
|
||||||
|
limit: 20,
|
||||||
|
});
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
const { Application } = require('@nocobase/server');
|
const { Application } = require('@nocobase/server');
|
||||||
const dotenv = require('dotenv');
|
const dotenv = require('dotenv');
|
||||||
|
const { PerformanceObserver, createHistogram } = require('perf_hooks');
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@ -21,8 +22,14 @@ const app = new Application({
|
|||||||
resourcer: {
|
resourcer: {
|
||||||
prefix: '/api',
|
prefix: '/api',
|
||||||
},
|
},
|
||||||
|
logger: {
|
||||||
|
// skip: () => true,
|
||||||
|
// transports: ['console'],
|
||||||
|
// level: 'error',
|
||||||
|
},
|
||||||
acl: false,
|
acl: false,
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
perfHooks: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
app.db.collection({
|
app.db.collection({
|
||||||
@ -78,6 +85,13 @@ app.db.collection({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// const obs = new PerformanceObserver((items) => {
|
||||||
|
// items.getEntries().forEach((item) => {
|
||||||
|
// console.log(item);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// obs.observe({ entryTypes: ['measure'] });
|
||||||
|
|
||||||
app.listen(13030, (err) => {
|
app.listen(13030, (err) => {
|
||||||
console.log('nocobase-server: http://localhost:13030/api/users');
|
console.log('nocobase-server: http://localhost:13030/api/users');
|
||||||
});
|
});
|
||||||
|
@ -11,5 +11,6 @@ export async function getConfig() {
|
|||||||
plugins,
|
plugins,
|
||||||
cacheManager,
|
cacheManager,
|
||||||
logger,
|
logger,
|
||||||
|
perfHooks: process.env.ENABLE_PERF_HOOKS ? true : false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nocobase/cache": "0.17.0-alpha.4",
|
||||||
"@nocobase/actions": "0.17.0-alpha.4",
|
"@nocobase/actions": "0.17.0-alpha.4",
|
||||||
"@nocobase/database": "0.17.0-alpha.4",
|
"@nocobase/database": "0.17.0-alpha.4",
|
||||||
"@nocobase/resourcer": "0.17.0-alpha.4",
|
"@nocobase/resourcer": "0.17.0-alpha.4",
|
||||||
|
@ -5,9 +5,9 @@ import { Auth, AuthExtend } from './auth';
|
|||||||
import { JwtOptions, JwtService } from './base/jwt-service';
|
import { JwtOptions, JwtService } from './base/jwt-service';
|
||||||
import { ITokenBlacklistService } from './base/token-blacklist-service';
|
import { ITokenBlacklistService } from './base/token-blacklist-service';
|
||||||
|
|
||||||
type Storer = {
|
export interface Storer {
|
||||||
get: (name: string) => Promise<Model>;
|
get: (name: string) => Promise<Model>;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type AuthManagerOptions = {
|
export type AuthManagerOptions = {
|
||||||
authKey: string;
|
authKey: string;
|
||||||
@ -45,8 +45,8 @@ export class AuthManager {
|
|||||||
* @description Add a new authenticate type and the corresponding authenticator.
|
* @description Add a new authenticate type and the corresponding authenticator.
|
||||||
* The types will show in the authenticators list of the admin panel.
|
* The types will show in the authenticators list of the admin panel.
|
||||||
*
|
*
|
||||||
* @param {string} authType - The type of the authenticator. It is required to be unique.
|
* @param authType - The type of the authenticator. It is required to be unique.
|
||||||
* @param {AuthConfig} authConfig - Configurations of the kind of authenticator.
|
* @param authConfig - Configurations of the kind of authenticator.
|
||||||
*/
|
*/
|
||||||
registerTypes(authType: string, authConfig: AuthConfig) {
|
registerTypes(authType: string, authConfig: AuthConfig) {
|
||||||
this.authTypes.register(authType, authConfig);
|
this.authTypes.register(authType, authConfig);
|
||||||
@ -66,8 +66,8 @@ export class AuthManager {
|
|||||||
/**
|
/**
|
||||||
* get
|
* get
|
||||||
* @description Get authenticator instance by name.
|
* @description Get authenticator instance by name.
|
||||||
* @param {string} name - The name of the authenticator.
|
* @param name - The name of the authenticator.
|
||||||
* @return {Promise<Auth>} authenticator instance.
|
* @return authenticator instance.
|
||||||
*/
|
*/
|
||||||
async get(name: string, ctx: Context) {
|
async get(name: string, ctx: Context) {
|
||||||
if (!this.storer) {
|
if (!this.storer) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Collection, Model } from '@nocobase/database';
|
import { Collection, Model } from '@nocobase/database';
|
||||||
import { Auth, AuthConfig } from '../auth';
|
import { Auth, AuthConfig } from '../auth';
|
||||||
import { JwtService } from './jwt-service';
|
import { JwtService } from './jwt-service';
|
||||||
|
import { Cache } from '@nocobase/cache';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BaseAuth
|
* BaseAuth
|
||||||
@ -35,6 +36,10 @@ export class BaseAuth extends Auth {
|
|||||||
return this.ctx.state.currentUser;
|
return this.ctx.state.currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCacheKey(userId: number) {
|
||||||
|
return `auth:${userId}`;
|
||||||
|
}
|
||||||
|
|
||||||
validateUsername(username: string) {
|
validateUsername(username: string) {
|
||||||
return /^[^@.<>"'/]{2,16}$/.test(username);
|
return /^[^@.<>"'/]{2,16}$/.test(username);
|
||||||
}
|
}
|
||||||
@ -51,11 +56,15 @@ export class BaseAuth extends Auth {
|
|||||||
this.ctx.headers['x-role'] = roleName;
|
this.ctx.headers['x-role'] = roleName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.userRepository.findOne({
|
const cache = this.ctx.cache as Cache;
|
||||||
filter: {
|
return await cache.wrap(this.getCacheKey(userId), () =>
|
||||||
id: userId,
|
this.userRepository.findOne({
|
||||||
},
|
filter: {
|
||||||
});
|
id: userId,
|
||||||
|
},
|
||||||
|
raw: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.ctx.logger.error(err);
|
this.ctx.logger.error(err);
|
||||||
return null;
|
return null;
|
||||||
@ -71,7 +80,6 @@ export class BaseAuth extends Auth {
|
|||||||
try {
|
try {
|
||||||
user = await this.validate();
|
user = await this.validate();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
|
||||||
this.ctx.throw(401, err.message);
|
this.ctx.throw(401, err.message);
|
||||||
}
|
}
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -91,6 +99,9 @@ export class BaseAuth extends Auth {
|
|||||||
if (!token) {
|
if (!token) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { userId } = await this.jwt.decode(token);
|
||||||
|
await this.ctx.app.emitAsync('beforeSignOut', { userId });
|
||||||
|
await this.ctx.cache.del(this.getCacheKey(userId));
|
||||||
return await this.jwt.block(token);
|
return await this.jwt.block(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
packages/core/cache/package.json
vendored
1
packages/core/cache/package.json
vendored
@ -6,6 +6,7 @@
|
|||||||
"main": "./lib/index.js",
|
"main": "./lib/index.js",
|
||||||
"types": "./lib/index.d.ts",
|
"types": "./lib/index.d.ts",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bloom-filters": "^3.0.1",
|
||||||
"cache-manager": "^5.2.4",
|
"cache-manager": "^5.2.4",
|
||||||
"cache-manager-redis-yet": "^4.1.2"
|
"cache-manager-redis-yet": "^4.1.2"
|
||||||
},
|
},
|
||||||
|
24
packages/core/cache/src/__tests__/bloom-filter.test.ts
vendored
Normal file
24
packages/core/cache/src/__tests__/bloom-filter.test.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { BloomFilter } from '../bloom-filter';
|
||||||
|
import { CacheManager } from '../cache-manager';
|
||||||
|
|
||||||
|
describe('bloomFilter', () => {
|
||||||
|
let bloomFilter: BloomFilter;
|
||||||
|
let cacheManager: CacheManager;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
cacheManager = new CacheManager();
|
||||||
|
cacheManager.registerStore({ name: 'memory', store: 'memory' });
|
||||||
|
bloomFilter = await cacheManager.createBloomFilter({ store: 'memory' });
|
||||||
|
await bloomFilter.reserve('bloom-test', 0.01, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await cacheManager.flushAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add and check', async () => {
|
||||||
|
await bloomFilter.add('bloom-test', 'hello');
|
||||||
|
expect(await bloomFilter.exists('bloom-test', 'hello')).toBeTruthy();
|
||||||
|
expect(await bloomFilter.exists('bloom-test', 'world')).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
6
packages/core/cache/src/bloom-filter/index.ts
vendored
Normal file
6
packages/core/cache/src/bloom-filter/index.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export interface BloomFilter {
|
||||||
|
reserve(key: string, errorRate: number, capacity: number): Promise<void>;
|
||||||
|
add(key: string, val: string): Promise<void>;
|
||||||
|
mAdd(key: string, vals: string[]): Promise<void>;
|
||||||
|
exists(key: string, val: string): Promise<boolean>;
|
||||||
|
}
|
39
packages/core/cache/src/bloom-filter/memory-bloom-filter.ts
vendored
Normal file
39
packages/core/cache/src/bloom-filter/memory-bloom-filter.ts
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { BloomFilter as IBloomFilter } from '.';
|
||||||
|
import { Cache } from '../cache';
|
||||||
|
import { BloomFilter } from 'bloom-filters';
|
||||||
|
|
||||||
|
export class MemoryBloomFilter implements IBloomFilter {
|
||||||
|
cache: Cache;
|
||||||
|
constructor(cache: Cache) {
|
||||||
|
this.cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
async reserve(key: string, errorRate: number, capacity: number) {
|
||||||
|
const filter = BloomFilter.create(capacity, errorRate);
|
||||||
|
await this.cache.set(key, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(key: string, value: string) {
|
||||||
|
const filter = (await this.cache.get(key)) as BloomFilter;
|
||||||
|
if (!filter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
filter.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async mAdd(key: string, values: string[]) {
|
||||||
|
const filter = (await this.cache.get(key)) as BloomFilter;
|
||||||
|
if (!filter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
values.forEach((value) => filter.add(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(key: string, value: string) {
|
||||||
|
const filter = (await this.cache.get(key)) as BloomFilter;
|
||||||
|
if (!filter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return filter.has(value);
|
||||||
|
}
|
||||||
|
}
|
34
packages/core/cache/src/bloom-filter/redis-bloom-filter.ts
vendored
Normal file
34
packages/core/cache/src/bloom-filter/redis-bloom-filter.ts
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { RedisStore } from 'cache-manager-redis-yet';
|
||||||
|
import { BloomFilter } from '.';
|
||||||
|
import { Cache } from '../cache';
|
||||||
|
|
||||||
|
export class RedisBloomFilter implements BloomFilter {
|
||||||
|
cache: Cache;
|
||||||
|
constructor(cache: Cache) {
|
||||||
|
this.cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStore() {
|
||||||
|
return this.cache.store.store as RedisStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
async reserve(key: string, errorRate: number, capacity: number) {
|
||||||
|
const store = this.getStore();
|
||||||
|
await store.client.bf.reserve(key, errorRate, capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(key: string, value: string) {
|
||||||
|
const store = this.getStore();
|
||||||
|
await store.client.bf.add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async mAdd(key: string, values: string[]) {
|
||||||
|
const store = this.getStore();
|
||||||
|
await store.client.bf.mAdd(key, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(key: string, value: string) {
|
||||||
|
const store = this.getStore();
|
||||||
|
return await store.client.bf.exists(key, value);
|
||||||
|
}
|
||||||
|
}
|
22
packages/core/cache/src/cache-manager.ts
vendored
22
packages/core/cache/src/cache-manager.ts
vendored
@ -3,6 +3,9 @@ import { Cache } from './cache';
|
|||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import { RedisStore, redisStore } from 'cache-manager-redis-yet';
|
import { RedisStore, redisStore } from 'cache-manager-redis-yet';
|
||||||
import deepmerge from 'deepmerge';
|
import deepmerge from 'deepmerge';
|
||||||
|
import { MemoryBloomFilter } from './bloom-filter/memory-bloom-filter';
|
||||||
|
import { BloomFilter } from './bloom-filter';
|
||||||
|
import { RedisBloomFilter } from './bloom-filter/redis-bloom-filter';
|
||||||
|
|
||||||
type StoreOptions = {
|
type StoreOptions = {
|
||||||
store?: 'memory' | FactoryStore<Store, any>;
|
store?: 'memory' | FactoryStore<Store, any>;
|
||||||
@ -118,4 +121,23 @@ export class CacheManager {
|
|||||||
}
|
}
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createBloomFilter(options?: { store?: string }): Promise<BloomFilter> {
|
||||||
|
const name = 'bloom-filter';
|
||||||
|
const { store = this.defaultStore } = options || {};
|
||||||
|
let cache: Cache;
|
||||||
|
try {
|
||||||
|
cache = this.getCache(name);
|
||||||
|
} catch (error) {
|
||||||
|
cache = await this.createCache({ name, store });
|
||||||
|
}
|
||||||
|
switch (store) {
|
||||||
|
case 'memory':
|
||||||
|
return new MemoryBloomFilter(cache);
|
||||||
|
case 'redis':
|
||||||
|
return new RedisBloomFilter(cache);
|
||||||
|
default:
|
||||||
|
throw new Error(`BloomFilter store [${store}] is not supported`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
6
packages/core/cache/src/cache.ts
vendored
6
packages/core/cache/src/cache.ts
vendored
@ -97,4 +97,10 @@ export class Cache {
|
|||||||
const object = (await this.get(key)) || {};
|
const object = (await this.get(key)) || {};
|
||||||
return object[objectKey];
|
return object[objectKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async delValueInObject(key: string, objectKey: string) {
|
||||||
|
const object = (await this.get(key)) || {};
|
||||||
|
delete object[objectKey];
|
||||||
|
await this.set(key, object);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
1
packages/core/cache/src/index.ts
vendored
1
packages/core/cache/src/index.ts
vendored
@ -1,2 +1,3 @@
|
|||||||
export * from './cache-manager';
|
export * from './cache-manager';
|
||||||
export * from './cache';
|
export * from './cache';
|
||||||
|
export * from './bloom-filter';
|
||||||
|
@ -208,7 +208,7 @@ export class EagerLoadingTree {
|
|||||||
include: includeForFilter,
|
include: includeForFilter,
|
||||||
} as any)
|
} as any)
|
||||||
).map((row) => {
|
).map((row) => {
|
||||||
return { row, pk: row.get(primaryKeyField) };
|
return { row, pk: row[primaryKeyField] };
|
||||||
});
|
});
|
||||||
|
|
||||||
const findOptions = {
|
const findOptions = {
|
||||||
|
@ -385,7 +385,7 @@ export async function updateMultipleAssociation(
|
|||||||
const createAccessor = association.accessors.create;
|
const createAccessor = association.accessors.create;
|
||||||
|
|
||||||
if (isUndefinedOrNull(value)) {
|
if (isUndefinedOrNull(value)) {
|
||||||
await model[setAccessor](null, { transaction, context });
|
await model[setAccessor](null, { transaction, context, individualHooks: true });
|
||||||
model.setDataValue(key, null);
|
model.setDataValue(key, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,7 @@ export function createAppLogger(options: AppLoggerOptions = {}) {
|
|||||||
}
|
}
|
||||||
info['req'] = pick(info['req'], requestWhitelist);
|
info['req'] = pick(info['req'], requestWhitelist);
|
||||||
info['res'] = pick(info['res'], responseWhitelist);
|
info['res'] = pick(info['res'], responseWhitelist);
|
||||||
|
|
||||||
ctx.logger.log(info);
|
ctx.logger.log(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { assign, MergeStrategies, requireModule } from '@nocobase/utils';
|
import { assign, MergeStrategies, prePerfHooksWrap, requireModule } from '@nocobase/utils';
|
||||||
import compose from 'koa-compose';
|
import compose from 'koa-compose';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import Middleware, { MiddlewareType } from './middleware';
|
import Middleware, { MiddlewareType } from './middleware';
|
||||||
import Resource from './resource';
|
import Resource from './resource';
|
||||||
import { HandlerType } from './resourcer';
|
import { HandlerType } from './resourcer';
|
||||||
|
import { RecordableHistogram, performance } from 'perf_hooks';
|
||||||
|
|
||||||
export type ActionType = string | HandlerType | ActionOptions;
|
export type ActionType = string | HandlerType | ActionOptions;
|
||||||
|
|
||||||
@ -309,6 +310,8 @@ export class Action {
|
|||||||
this.getHandler(),
|
this.getHandler(),
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
|
// handlers = handlers.map((handler) => prePerfHooksWrap(handler));
|
||||||
|
|
||||||
return handlers;
|
return handlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,11 +20,19 @@ import { createCacheManager } from './cache';
|
|||||||
import { registerCli } from './commands';
|
import { registerCli } from './commands';
|
||||||
import { CronJobManager } from './cron/cron-job-manager';
|
import { CronJobManager } from './cron/cron-job-manager';
|
||||||
import { ApplicationNotInstall } from './errors/application-not-install';
|
import { ApplicationNotInstall } from './errors/application-not-install';
|
||||||
import { createAppProxy, createI18n, createResourcer, getCommandFullName, registerMiddlewares } from './helper';
|
import {
|
||||||
|
createAppProxy,
|
||||||
|
createI18n,
|
||||||
|
createResourcer,
|
||||||
|
getCommandFullName,
|
||||||
|
registerMiddlewares,
|
||||||
|
enablePerfHooks,
|
||||||
|
} from './helper';
|
||||||
import { ApplicationVersion } from './helpers/application-version';
|
import { ApplicationVersion } from './helpers/application-version';
|
||||||
import { Locale } from './locale';
|
import { Locale } from './locale';
|
||||||
import { Plugin } from './plugin';
|
import { Plugin } from './plugin';
|
||||||
import { InstallOptions, PluginManager } from './plugin-manager';
|
import { InstallOptions, PluginManager } from './plugin-manager';
|
||||||
|
import { RecordableHistogram, performance } from 'node:perf_hooks';
|
||||||
|
|
||||||
const packageJson = require('../package.json');
|
const packageJson = require('../package.json');
|
||||||
|
|
||||||
@ -50,6 +58,7 @@ export interface ApplicationOptions {
|
|||||||
pmSock?: string;
|
pmSock?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
authManager?: AuthManagerOptions;
|
authManager?: AuthManagerOptions;
|
||||||
|
perfHooks?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DefaultState extends KoaDefaultState {
|
export interface DefaultState extends KoaDefaultState {
|
||||||
@ -115,6 +124,7 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
name: string;
|
name: string;
|
||||||
} = null;
|
} = null;
|
||||||
public running = false;
|
public running = false;
|
||||||
|
public perfHistograms = new Map<string, RecordableHistogram>();
|
||||||
protected plugins = new Map<string, Plugin>();
|
protected plugins = new Map<string, Plugin>();
|
||||||
protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance();
|
protected _appSupervisor: AppSupervisor = AppSupervisor.getInstance();
|
||||||
protected _started: boolean;
|
protected _started: boolean;
|
||||||
@ -560,6 +570,10 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
this.log.error(e);
|
this.log.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._cacheManager) {
|
||||||
|
await this._cacheManager.close();
|
||||||
|
}
|
||||||
|
|
||||||
await this.emitAsync('afterStop', this, options);
|
await this.emitAsync('afterStop', this, options);
|
||||||
|
|
||||||
this.stopped = true;
|
this.stopped = true;
|
||||||
@ -729,6 +743,10 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
|
|
||||||
this._locales = new Locale(createAppProxy(this));
|
this._locales = new Locale(createAppProxy(this));
|
||||||
|
|
||||||
|
if (options.perfHooks) {
|
||||||
|
enablePerfHooks(this);
|
||||||
|
}
|
||||||
|
|
||||||
registerMiddlewares(this, options);
|
registerMiddlewares(this, options);
|
||||||
|
|
||||||
if (options.registerActions !== false) {
|
if (options.registerActions !== false) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import cors from '@koa/cors';
|
import cors from '@koa/cors';
|
||||||
import Database from '@nocobase/database';
|
import Database from '@nocobase/database';
|
||||||
import { Resourcer } from '@nocobase/resourcer';
|
import { Resourcer } from '@nocobase/resourcer';
|
||||||
import { uid } from '@nocobase/utils';
|
import { postPerfHooksWrap, prePerfHooksWrap, uid } from '@nocobase/utils';
|
||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
@ -13,6 +13,7 @@ import { dateTemplate } from './middlewares/data-template';
|
|||||||
import { dataWrapping } from './middlewares/data-wrapping';
|
import { dataWrapping } from './middlewares/data-wrapping';
|
||||||
import { db2resource } from './middlewares/db2resource';
|
import { db2resource } from './middlewares/db2resource';
|
||||||
import { i18n } from './middlewares/i18n';
|
import { i18n } from './middlewares/i18n';
|
||||||
|
import { createHistogram, RecordableHistogram } from 'perf_hooks';
|
||||||
|
|
||||||
export function createI18n(options: ApplicationOptions) {
|
export function createI18n(options: ApplicationOptions) {
|
||||||
const instance = i18next.createInstance();
|
const instance = i18next.createInstance();
|
||||||
@ -80,7 +81,10 @@ export function registerMiddlewares(app: Application, options: ApplicationOption
|
|||||||
app.use(dataWrapping(), { tag: 'dataWrapping', after: 'i18n' });
|
app.use(dataWrapping(), { tag: 'dataWrapping', after: 'i18n' });
|
||||||
}
|
}
|
||||||
|
|
||||||
app.resourcer.use(parseVariables, { tag: 'parseVariables', after: 'acl' });
|
app.resourcer.use(parseVariables, {
|
||||||
|
tag: 'parseVariables',
|
||||||
|
after: 'acl',
|
||||||
|
});
|
||||||
app.resourcer.use(dateTemplate, { tag: 'dateTemplate', after: 'acl' });
|
app.resourcer.use(dateTemplate, { tag: 'dateTemplate', after: 'acl' });
|
||||||
|
|
||||||
app.use(db2resource, { tag: 'db2resource', after: 'dataWrapping' });
|
app.use(db2resource, { tag: 'db2resource', after: 'dataWrapping' });
|
||||||
@ -119,3 +123,35 @@ export const tsxRerunning = async () => {
|
|||||||
const file = resolve(process.cwd(), 'storage/app.watch.ts');
|
const file = resolve(process.cwd(), 'storage/app.watch.ts');
|
||||||
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
|
await fs.promises.writeFile(file, `export const watchId = '${uid()}';`, 'utf-8');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const enablePerfHooks = (app: Application) => {
|
||||||
|
app.context.getPerfHistogram = (name: string) => {
|
||||||
|
if (!app.perfHistograms.has(name)) {
|
||||||
|
app.perfHistograms.set(name, createHistogram());
|
||||||
|
}
|
||||||
|
return app.perfHistograms.get(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
app.resourcer.define({
|
||||||
|
name: 'perf',
|
||||||
|
actions: {
|
||||||
|
view: async (ctx, next) => {
|
||||||
|
const result = {};
|
||||||
|
const histograms = ctx.app.perfHistograms as Map<string, RecordableHistogram>;
|
||||||
|
const sortedHistograms = [...histograms.entries()].sort(([i, a], [j, b]) => b.mean - a.mean);
|
||||||
|
sortedHistograms.forEach(([name, histogram]) => {
|
||||||
|
result[name] = histogram;
|
||||||
|
});
|
||||||
|
ctx.body = result;
|
||||||
|
await next();
|
||||||
|
},
|
||||||
|
reset: async (ctx, next) => {
|
||||||
|
const histograms = ctx.app.perfHistograms as Map<string, RecordableHistogram>;
|
||||||
|
histograms.forEach((histogram: RecordableHistogram) => histogram.reset());
|
||||||
|
await next();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.acl.allow('perf', '*', 'public');
|
||||||
|
};
|
||||||
|
@ -9,6 +9,7 @@ export class Locale {
|
|||||||
defaultLang = 'en-US';
|
defaultLang = 'en-US';
|
||||||
localeFn = new Map();
|
localeFn = new Map();
|
||||||
resourceCached = new Map();
|
resourceCached = new Map();
|
||||||
|
i18nInstances = new Map();
|
||||||
|
|
||||||
constructor(app: Application) {
|
constructor(app: Application) {
|
||||||
this.app = app;
|
this.app = app;
|
||||||
@ -97,4 +98,16 @@ export class Locale {
|
|||||||
});
|
});
|
||||||
return resources;
|
return resources;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getI18nInstance(lang: string) {
|
||||||
|
if (lang === '*' || !lang) {
|
||||||
|
return this.app.i18n.cloneInstance({ initImmediate: false });
|
||||||
|
}
|
||||||
|
let instance = this.i18nInstances.get(lang);
|
||||||
|
if (!instance) {
|
||||||
|
instance = this.app.i18n.cloneInstance({ initImmediate: false });
|
||||||
|
this.i18nInstances.set(lang, instance);
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
|
import { Locale } from '../locale';
|
||||||
|
|
||||||
export async function i18n(ctx, next) {
|
export async function i18n(ctx, next) {
|
||||||
const i18n = ctx.app.i18n.cloneInstance({ initImmediate: false });
|
|
||||||
ctx.i18n = i18n;
|
|
||||||
ctx.t = i18n.t.bind(i18n);
|
|
||||||
ctx.getCurrentLocale = () => {
|
ctx.getCurrentLocale = () => {
|
||||||
const lng =
|
const lng =
|
||||||
ctx.get('X-Locale') ||
|
ctx.get('X-Locale') ||
|
||||||
@ -12,9 +11,13 @@ export async function i18n(ctx, next) {
|
|||||||
return lng;
|
return lng;
|
||||||
};
|
};
|
||||||
const lng = ctx.getCurrentLocale();
|
const lng = ctx.getCurrentLocale();
|
||||||
|
const localeManager = ctx.app.localeManager as Locale;
|
||||||
|
const i18n = await localeManager.getI18nInstance(lng);
|
||||||
|
ctx.i18n = i18n;
|
||||||
|
ctx.t = i18n.t.bind(i18n);
|
||||||
if (lng !== '*' && lng) {
|
if (lng !== '*' && lng) {
|
||||||
i18n.changeLanguage(lng);
|
i18n.changeLanguage(lng);
|
||||||
await ctx.app.localeManager.loadResourcesByLang(lng);
|
await localeManager.loadResourcesByLang(lng);
|
||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
|
@ -21,5 +21,6 @@ export * from './toposort';
|
|||||||
export * from './uid';
|
export * from './uid';
|
||||||
export * from './url';
|
export * from './url';
|
||||||
export * from './measure-execution-time';
|
export * from './measure-execution-time';
|
||||||
|
export * from './perf-hooks';
|
||||||
|
|
||||||
export { dayjs, lodash };
|
export { dayjs, lodash };
|
||||||
|
32
packages/core/utils/src/perf-hooks.ts
Normal file
32
packages/core/utils/src/perf-hooks.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { RecordableHistogram, performance } from 'perf_hooks';
|
||||||
|
|
||||||
|
export const prePerfHooksWrap = (handler: any, options?: { name?: string }) => {
|
||||||
|
const { name } = options || {};
|
||||||
|
return async (ctx: any, next: any) => {
|
||||||
|
if (!ctx.getPerfHistogram) {
|
||||||
|
return await handler(ctx, next);
|
||||||
|
}
|
||||||
|
const histogram = ctx.getPerfHistogram(name || handler) as RecordableHistogram;
|
||||||
|
const start = performance.now();
|
||||||
|
await handler(ctx, async () => {
|
||||||
|
const duration = performance.now() - start;
|
||||||
|
histogram.record(Math.ceil(duration * 1e6));
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const postPerfHooksWrap = (handler: any, options: { name?: string }) => {
|
||||||
|
const { name } = options || {};
|
||||||
|
return async (ctx: any, next: any) => {
|
||||||
|
if (!ctx.getPerfHistogram) {
|
||||||
|
return await handler(ctx, next);
|
||||||
|
}
|
||||||
|
await next();
|
||||||
|
const histogram = ctx.getPerfHistogram(name || handler) as RecordableHistogram;
|
||||||
|
const start = performance.now();
|
||||||
|
await handler(ctx, async () => {});
|
||||||
|
const duration = performance.now() - start;
|
||||||
|
histogram.record(Math.ceil(duration * 1e6));
|
||||||
|
};
|
||||||
|
};
|
@ -21,7 +21,8 @@
|
|||||||
"@nocobase/database": "0.x",
|
"@nocobase/database": "0.x",
|
||||||
"@nocobase/server": "0.x",
|
"@nocobase/server": "0.x",
|
||||||
"@nocobase/test": "0.x",
|
"@nocobase/test": "0.x",
|
||||||
"@nocobase/utils": "0.x"
|
"@nocobase/utils": "0.x",
|
||||||
|
"@nocobase/cache": "0.x"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -19,6 +19,7 @@ describe('role', () => {
|
|||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
db,
|
db,
|
||||||
|
cache: api.cache,
|
||||||
state: {
|
state: {
|
||||||
currentRole: '',
|
currentRole: '',
|
||||||
},
|
},
|
||||||
@ -83,4 +84,153 @@ describe('role', () => {
|
|||||||
await setCurrentRole(ctx, () => {});
|
await setCurrentRole(ctx, () => {});
|
||||||
expect(ctx.state.currentRole).toBe('anonymous');
|
expect(ctx.state.currentRole).toBe('anonymous');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set role in cache', async () => {
|
||||||
|
ctx.state.currentUser = await db.getRepository('users').findOne({
|
||||||
|
appends: ['roles'],
|
||||||
|
});
|
||||||
|
ctx.get = function (name) {
|
||||||
|
if (name === 'X-Role') {
|
||||||
|
return 'admin';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await setCurrentRole(ctx, () => {});
|
||||||
|
const roles = await ctx.cache.get(`roles:${ctx.state.currentUser.id}`);
|
||||||
|
expect(roles).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update cache when role added', async () => {
|
||||||
|
ctx.get = function (name) {
|
||||||
|
if (name === 'X-Role') {
|
||||||
|
return 'admin';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await db.getRepository('roles').create({
|
||||||
|
values: {
|
||||||
|
name: 'test',
|
||||||
|
title: 'Test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ctx.state.currentUser = await db.getRepository('users').findOne({
|
||||||
|
appends: ['roles'],
|
||||||
|
});
|
||||||
|
await setCurrentRole(ctx, () => {});
|
||||||
|
let roles = await ctx.cache.get(`roles:${ctx.state.currentUser.id}`);
|
||||||
|
expect(roles).toBeDefined();
|
||||||
|
let testRole = roles.find((role) => role.name === 'test');
|
||||||
|
expect(testRole).toBeUndefined();
|
||||||
|
|
||||||
|
await db.getRepository('users').update({
|
||||||
|
values: {
|
||||||
|
roles: [
|
||||||
|
...ctx.state.currentUser.roles,
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
filterByTk: ctx.state.currentUser.id,
|
||||||
|
});
|
||||||
|
roles = await ctx.cache.get(`roles:${ctx.state.currentUser.id}`);
|
||||||
|
expect(roles).toBeUndefined();
|
||||||
|
await setCurrentRole(ctx, () => {});
|
||||||
|
roles = await ctx.cache.get(`roles:${ctx.state.currentUser.id}`);
|
||||||
|
expect(roles).toBeDefined();
|
||||||
|
testRole = roles.find((role) => role.name === 'test');
|
||||||
|
expect(testRole).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update cache when one role removed', async () => {
|
||||||
|
ctx.get = function (name) {
|
||||||
|
if (name === 'X-Role') {
|
||||||
|
return 'admin';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ctx.state.currentUser = await db.getRepository('users').findOne({
|
||||||
|
appends: ['roles'],
|
||||||
|
});
|
||||||
|
await setCurrentRole(ctx, () => {});
|
||||||
|
let roles = await ctx.cache.get(`roles:${ctx.state.currentUser.id}`);
|
||||||
|
expect(roles).toBeDefined();
|
||||||
|
let testRole = roles.find((role) => role.name === 'member');
|
||||||
|
expect(testRole).toBeDefined();
|
||||||
|
|
||||||
|
await db.getRepository('users').update({
|
||||||
|
values: {
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
name: 'root',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'admin',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
filterByTk: ctx.state.currentUser.id,
|
||||||
|
});
|
||||||
|
roles = await ctx.cache.get(`roles:${ctx.state.currentUser.id}`);
|
||||||
|
expect(roles).toBeUndefined();
|
||||||
|
await setCurrentRole(ctx, () => {});
|
||||||
|
roles = await ctx.cache.get(`roles:${ctx.state.currentUser.id}`);
|
||||||
|
expect(roles).toBeDefined();
|
||||||
|
testRole = roles.find((role) => role.name === 'member');
|
||||||
|
expect(testRole).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update cache when all roles removed', async () => {
|
||||||
|
ctx.get = function (name) {
|
||||||
|
if (name === 'X-Role') {
|
||||||
|
return 'admin';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ctx.state.currentUser = await db.getRepository('users').findOne({
|
||||||
|
appends: ['roles'],
|
||||||
|
});
|
||||||
|
await setCurrentRole(ctx, () => {});
|
||||||
|
let roles = await ctx.cache.get(`roles:${ctx.state.currentUser.id}`);
|
||||||
|
expect(roles).toBeDefined();
|
||||||
|
|
||||||
|
await db.getRepository('users').update({
|
||||||
|
values: {
|
||||||
|
roles: null,
|
||||||
|
},
|
||||||
|
filterByTk: ctx.state.currentUser.id,
|
||||||
|
});
|
||||||
|
roles = await ctx.cache.get(`roles:${ctx.state.currentUser.id}`);
|
||||||
|
expect(roles).toBeUndefined();
|
||||||
|
const throwFn = jest.fn();
|
||||||
|
ctx.throw = throwFn;
|
||||||
|
await setCurrentRole(ctx, () => {});
|
||||||
|
expect(throwFn).lastCalledWith(401, { code: 'ROLE_NOT_FOUND_ERR', message: 'The user role does not exist.' });
|
||||||
|
expect(ctx.state.currentRole).not.toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update cache when role deleted', async () => {
|
||||||
|
ctx.get = function (name) {
|
||||||
|
if (name === 'X-Role') {
|
||||||
|
return 'admin';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ctx.state.currentUser = await db.getRepository('users').findOne({
|
||||||
|
appends: ['roles'],
|
||||||
|
});
|
||||||
|
await setCurrentRole(ctx, () => {});
|
||||||
|
let roles = await ctx.cache.get(`roles:${ctx.state.currentUser.id}`);
|
||||||
|
expect(roles).toBeDefined();
|
||||||
|
let testRole = roles.find((role) => role.name === 'member');
|
||||||
|
expect(testRole).toBeDefined();
|
||||||
|
|
||||||
|
await db.getRepository('roles').destroy({
|
||||||
|
filter: {
|
||||||
|
name: 'member',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
roles = await ctx.cache.get(`roles:${ctx.state.currentUser.id}`);
|
||||||
|
expect(roles).toBeUndefined();
|
||||||
|
await setCurrentRole(ctx, () => {});
|
||||||
|
roles = await ctx.cache.get(`roles:${ctx.state.currentUser.id}`);
|
||||||
|
expect(roles).toBeDefined();
|
||||||
|
testRole = roles.find((role) => role.name === 'member');
|
||||||
|
expect(testRole).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -83,6 +83,7 @@ describe('actions', () => {
|
|||||||
await db.getRepository('roles').destroy({
|
await db.getRepository('roles').destroy({
|
||||||
filterByTk: 'test',
|
filterByTk: 'test',
|
||||||
});
|
});
|
||||||
|
await app.cache.reset();
|
||||||
|
|
||||||
response = await agent.post('/auth:signIn').send({
|
response = await agent.post('/auth:signIn').send({
|
||||||
email: 'test2@nocobase.com',
|
email: 'test2@nocobase.com',
|
||||||
|
@ -22,7 +22,7 @@ export async function setDefaultRole(ctx: Context, next: Next) {
|
|||||||
await db.sequelize.transaction(async (transaction) => {
|
await db.sequelize.transaction(async (transaction) => {
|
||||||
await repository.update({
|
await repository.update({
|
||||||
filter: {
|
filter: {
|
||||||
userId: currentUser.get('id'),
|
userId: currentUser.id,
|
||||||
},
|
},
|
||||||
values: {
|
values: {
|
||||||
default: false,
|
default: false,
|
||||||
@ -31,7 +31,7 @@ export async function setDefaultRole(ctx: Context, next: Next) {
|
|||||||
});
|
});
|
||||||
await repository.update({
|
await repository.update({
|
||||||
filter: {
|
filter: {
|
||||||
userId: currentUser.get('id'),
|
userId: currentUser.id,
|
||||||
roleName,
|
roleName,
|
||||||
},
|
},
|
||||||
values: {
|
values: {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Context } from '@nocobase/actions';
|
import { Context } from '@nocobase/actions';
|
||||||
import { Repository } from '@nocobase/database';
|
import { Cache } from '@nocobase/cache';
|
||||||
|
import { Model, Repository } from '@nocobase/database';
|
||||||
|
|
||||||
export async function setCurrentRole(ctx: Context, next) {
|
export async function setCurrentRole(ctx: Context, next) {
|
||||||
const currentRole = ctx.get('X-Role');
|
const currentRole = ctx.get('X-Role');
|
||||||
@ -13,9 +14,14 @@ export async function setCurrentRole(ctx: Context, next) {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cache = ctx.cache as Cache;
|
||||||
const repository = ctx.db.getRepository('users.roles', ctx.state.currentUser.id) as unknown as Repository;
|
const repository = ctx.db.getRepository('users.roles', ctx.state.currentUser.id) as unknown as Repository;
|
||||||
const roles = await repository.find();
|
const roles = (await cache.wrap(`roles:${ctx.state.currentUser.id}`, () =>
|
||||||
ctx.state.currentUser.setDataValue('roles', roles);
|
repository.find({
|
||||||
|
raw: true,
|
||||||
|
}),
|
||||||
|
)) as Model[];
|
||||||
|
ctx.state.currentUser.roles = roles;
|
||||||
|
|
||||||
// 1. If the X-Role is set, use the specified role
|
// 1. If the X-Role is set, use the specified role
|
||||||
if (currentRole) {
|
if (currentRole) {
|
||||||
|
@ -13,6 +13,7 @@ import { setCurrentRole } from './middlewares/setCurrentRole';
|
|||||||
import { RoleModel } from './model/RoleModel';
|
import { RoleModel } from './model/RoleModel';
|
||||||
import { RoleResourceActionModel } from './model/RoleResourceActionModel';
|
import { RoleResourceActionModel } from './model/RoleResourceActionModel';
|
||||||
import { RoleResourceModel } from './model/RoleResourceModel';
|
import { RoleResourceModel } from './model/RoleResourceModel';
|
||||||
|
import { Cache } from '@nocobase/cache';
|
||||||
|
|
||||||
export interface AssociationFieldAction {
|
export interface AssociationFieldAction {
|
||||||
associationActions: string[];
|
associationActions: string[];
|
||||||
@ -347,6 +348,16 @@ export class PluginACL extends Plugin {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Delete cache when the roles of a user changed
|
||||||
|
this.app.db.on('rolesUsers.afterSave', async (model) => {
|
||||||
|
const cache = this.app.cache as Cache;
|
||||||
|
await cache.del(`roles:${model.get('userId')}`);
|
||||||
|
});
|
||||||
|
this.app.db.on('rolesUsers.afterDestroy', async (model) => {
|
||||||
|
const cache = this.app.cache as Cache;
|
||||||
|
await cache.del(`roles:${model.get('userId')}`);
|
||||||
|
});
|
||||||
|
|
||||||
const writeRolesToACL = async (app, options) => {
|
const writeRolesToACL = async (app, options) => {
|
||||||
const exists = await this.app.db.collectionExistsInDb('roles');
|
const exists = await this.app.db.collectionExistsInDb('roles');
|
||||||
if (exists) {
|
if (exists) {
|
||||||
@ -433,6 +444,9 @@ export class PluginACL extends Plugin {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.app.on('beforeSignOut', ({ userId }) => {
|
||||||
|
this.app.cache.del(`roles:${userId}`);
|
||||||
|
});
|
||||||
this.app.resourcer.use(setCurrentRole, { tag: 'setCurrentRole', before: 'acl', after: 'auth' });
|
this.app.resourcer.use(setCurrentRole, { tag: 'setCurrentRole', before: 'acl', after: 'auth' });
|
||||||
|
|
||||||
this.app.acl.allow('users', 'setDefaultRole', 'loggedIn');
|
this.app.acl.allow('users', 'setDefaultRole', 'loggedIn');
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
import { Database, Model } from '@nocobase/database';
|
||||||
|
import { MockServer, mockServer } from '@nocobase/test';
|
||||||
|
import { BaseAuth } from '@nocobase/auth';
|
||||||
|
|
||||||
|
describe('auth', () => {
|
||||||
|
let auth: BaseAuth;
|
||||||
|
let app: MockServer;
|
||||||
|
let db: Database;
|
||||||
|
let user: Model;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
app = mockServer({
|
||||||
|
plugins: ['users', 'auth'],
|
||||||
|
});
|
||||||
|
await app.quickstart({ clean: true });
|
||||||
|
db = app.db;
|
||||||
|
|
||||||
|
user = await db.getRepository('users').create({
|
||||||
|
values: {
|
||||||
|
username: 'admin',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const jwt = app.authManager.jwt;
|
||||||
|
auth = new BaseAuth({
|
||||||
|
userCollection: db.getCollection('users'),
|
||||||
|
ctx: {
|
||||||
|
app,
|
||||||
|
getBearerToken() {
|
||||||
|
return jwt.sign({ userId: user.id });
|
||||||
|
},
|
||||||
|
cache: app.cache,
|
||||||
|
} as any,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
await app.cache.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app.cache.reset();
|
||||||
|
await app.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get user from cache', async () => {
|
||||||
|
expect(await app.cache.get(auth.getCacheKey(user.id))).toBeUndefined();
|
||||||
|
let userData = await auth.check();
|
||||||
|
expect(userData).not.toBeNull();
|
||||||
|
expect(await app.cache.get(auth.getCacheKey(user.id))).toBeDefined();
|
||||||
|
userData = await auth.check();
|
||||||
|
expect(userData).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update cache when user changed', async () => {
|
||||||
|
await auth.check();
|
||||||
|
let cacheData = await app.cache.get(auth.getCacheKey(user.id));
|
||||||
|
expect(cacheData['nickname']).toBeNull();
|
||||||
|
await db.getRepository('users').update({
|
||||||
|
values: {
|
||||||
|
nickname: 'admin',
|
||||||
|
},
|
||||||
|
filterByTk: user.id,
|
||||||
|
});
|
||||||
|
cacheData = await app.cache.get(auth.getCacheKey(user.id));
|
||||||
|
console.log(cacheData);
|
||||||
|
expect(cacheData['nickname']).toBe('admin');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete cache when user deleted', async () => {
|
||||||
|
await auth.check();
|
||||||
|
let cacheData = await app.cache.get(auth.getCacheKey(user.id));
|
||||||
|
expect(cacheData['nickname']).toBeNull();
|
||||||
|
await db.getRepository('users').destroy({
|
||||||
|
filterByTk: user.id,
|
||||||
|
});
|
||||||
|
cacheData = await app.cache.get(auth.getCacheKey(user.id));
|
||||||
|
expect(cacheData).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,89 @@
|
|||||||
|
import { Cache, CacheManager } from '@nocobase/cache';
|
||||||
|
import { Storer } from '../storer';
|
||||||
|
|
||||||
|
class MockDB {
|
||||||
|
data: any;
|
||||||
|
hooks = {};
|
||||||
|
|
||||||
|
constructor(data: any) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
on(name: string, func: (...args: any[]) => Promise<void>) {
|
||||||
|
this.hooks[name] = func;
|
||||||
|
}
|
||||||
|
|
||||||
|
async emitAsync(name: string, ...args: any[]) {
|
||||||
|
const func = this.hooks[name];
|
||||||
|
if (func) {
|
||||||
|
await func(...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRepository() {
|
||||||
|
return {
|
||||||
|
find: async () => {
|
||||||
|
return this.data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('storer', () => {
|
||||||
|
let db: any;
|
||||||
|
let storer: Storer;
|
||||||
|
let cache: Cache;
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
name: 'test1',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'test2',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const cacheManager = new CacheManager();
|
||||||
|
cache = await cacheManager.createCache({ name: 'test' });
|
||||||
|
db = new MockDB(data);
|
||||||
|
storer = new Storer({ db, cache });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
db = undefined;
|
||||||
|
storer = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get authenticator from cache', async () => {
|
||||||
|
expect(await cache.get('authenticators')).toBeUndefined();
|
||||||
|
let authenticator = await storer.get('test1');
|
||||||
|
expect(authenticator).toBeDefined();
|
||||||
|
|
||||||
|
expect(await cache.get('authenticators')).toBeDefined();
|
||||||
|
authenticator = await storer.get('test1');
|
||||||
|
expect(authenticator).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete from cache on afterDestory', async () => {
|
||||||
|
expect(await storer.get('test1')).toBeDefined();
|
||||||
|
await db.emitAsync('authenticators.afterDestroy', data[0]);
|
||||||
|
const authenticators = await cache.get('authenticators');
|
||||||
|
expect(authenticators['test1']).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete from cache on afterSave as disabled', async () => {
|
||||||
|
expect(await storer.get('test1')).toBeDefined();
|
||||||
|
await db.emitAsync('authenticators.afterSave', { ...data[0], enabled: false });
|
||||||
|
const authenticators = await cache.get('authenticators');
|
||||||
|
expect(authenticators['test1']).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set cache on afterSave as enabled', async () => {
|
||||||
|
expect(await storer.get('test1')).toBeDefined();
|
||||||
|
await db.emitAsync('authenticators.afterSave', { name: 'test3', enabled: true });
|
||||||
|
const authenticators = await cache.get('authenticators');
|
||||||
|
expect(authenticators['test3']).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
@ -1,12 +1,12 @@
|
|||||||
import Database, { Repository } from '@nocobase/database';
|
import Database, { Repository } from '@nocobase/database';
|
||||||
import { MockServer, mockServer } from '@nocobase/test';
|
import { MockServer, mockServer } from '@nocobase/test';
|
||||||
import { TokenBlacklistService } from '../token-blacklist';
|
import { ITokenBlacklistService } from '@nocobase/auth';
|
||||||
|
|
||||||
describe('token-blacklist', () => {
|
describe('token-blacklist', () => {
|
||||||
let app: MockServer;
|
let app: MockServer;
|
||||||
let db: Database;
|
let db: Database;
|
||||||
let repo: Repository;
|
let repo: Repository;
|
||||||
let tokenBlacklist: TokenBlacklistService;
|
let tokenBlacklist: ITokenBlacklistService;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = mockServer({
|
app = mockServer({
|
||||||
@ -15,7 +15,7 @@ describe('token-blacklist', () => {
|
|||||||
await app.loadAndInstall({ clean: true });
|
await app.loadAndInstall({ clean: true });
|
||||||
db = app.db;
|
db = app.db;
|
||||||
repo = db.getRepository('tokenBlacklist');
|
repo = db.getRepository('tokenBlacklist');
|
||||||
tokenBlacklist = new TokenBlacklistService(app.getPlugin('auth'));
|
tokenBlacklist = app.authManager.jwt.blacklist;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
@ -8,8 +8,12 @@ import { BasicAuth } from './basic-auth';
|
|||||||
import { enUS, zhCN } from './locale';
|
import { enUS, zhCN } from './locale';
|
||||||
import { AuthModel } from './model/authenticator';
|
import { AuthModel } from './model/authenticator';
|
||||||
import { TokenBlacklistService } from './token-blacklist';
|
import { TokenBlacklistService } from './token-blacklist';
|
||||||
|
import { Cache } from '@nocobase/cache';
|
||||||
|
import { Storer } from './storer';
|
||||||
|
|
||||||
export class AuthPlugin extends Plugin {
|
export class AuthPlugin extends Plugin {
|
||||||
|
cache: Cache;
|
||||||
|
|
||||||
afterAdd() {}
|
afterAdd() {}
|
||||||
async beforeLoad() {
|
async beforeLoad() {
|
||||||
this.app.i18n.addResources('zh-CN', namespace, zhCN);
|
this.app.i18n.addResources('zh-CN', namespace, zhCN);
|
||||||
@ -30,16 +34,19 @@ export class AuthPlugin extends Plugin {
|
|||||||
plugin: this,
|
plugin: this,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// Set up auth manager and register preset auth type
|
this.cache = await this.app.cacheManager.createCache({
|
||||||
this.app.authManager.setStorer({
|
name: 'auth',
|
||||||
get: async (name: string) => {
|
prefix: 'auth',
|
||||||
const repo = this.db.getRepository('authenticators');
|
store: 'memory',
|
||||||
const authenticators = await repo.find({ filter: { enabled: true } });
|
|
||||||
const authenticator = authenticators.find((authenticator: Model) => authenticator.name === name);
|
|
||||||
return authenticator || authenticators[0];
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set up auth manager and register preset auth type
|
||||||
|
const storer = new Storer({
|
||||||
|
db: this.db,
|
||||||
|
cache: this.cache,
|
||||||
|
});
|
||||||
|
this.app.authManager.setStorer(storer);
|
||||||
|
|
||||||
if (!this.app.authManager.jwt.blacklist) {
|
if (!this.app.authManager.jwt.blacklist) {
|
||||||
// If blacklist service is not set, should configure default blacklist service
|
// If blacklist service is not set, should configure default blacklist service
|
||||||
this.app.authManager.setTokenBlacklistService(new TokenBlacklistService(this));
|
this.app.authManager.setTokenBlacklistService(new TokenBlacklistService(this));
|
||||||
@ -64,6 +71,16 @@ export class AuthPlugin extends Plugin {
|
|||||||
name: `pm.${this.name}.authenticators`,
|
name: `pm.${this.name}.authenticators`,
|
||||||
actions: ['authenticators:*'],
|
actions: ['authenticators:*'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Change cache when user changed
|
||||||
|
this.app.db.on('users.afterSave', async (user: Model) => {
|
||||||
|
const cache = this.app.cache as Cache;
|
||||||
|
await cache.set(`auth:${user.id}`, user.toJSON());
|
||||||
|
});
|
||||||
|
this.app.db.on('users.afterDestroy', async (user: Model) => {
|
||||||
|
const cache = this.app.cache as Cache;
|
||||||
|
await cache.del(`auth:${user.id}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async install(options?: InstallOptions) {
|
async install(options?: InstallOptions) {
|
||||||
|
53
packages/plugins/@nocobase/plugin-auth/src/server/storer.ts
Normal file
53
packages/plugins/@nocobase/plugin-auth/src/server/storer.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Storer as IStorer } from '@nocobase/auth';
|
||||||
|
import { Cache } from '@nocobase/cache';
|
||||||
|
import { Database, Model } from '@nocobase/database';
|
||||||
|
import { AuthModel } from './model/authenticator';
|
||||||
|
|
||||||
|
export class Storer implements IStorer {
|
||||||
|
db: Database;
|
||||||
|
cache: Cache;
|
||||||
|
key = 'authenticators';
|
||||||
|
|
||||||
|
constructor({ db, cache }: { db: Database; cache: Cache }) {
|
||||||
|
this.db = db;
|
||||||
|
this.cache = cache;
|
||||||
|
|
||||||
|
this.db.on('authenticators.afterSave', async (model: AuthModel) => {
|
||||||
|
if (!model.enabled) {
|
||||||
|
await this.cache.delValueInObject(this.key, model.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.cache.setValueInObject(this.key, model.name, model);
|
||||||
|
});
|
||||||
|
this.db.on('authenticators.afterDestroy', async (model: AuthModel) => {
|
||||||
|
await this.cache.delValueInObject(this.key, model.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCache(): Promise<AuthModel[]> {
|
||||||
|
const authenticators = (await this.cache.get(this.key)) as Record<string, AuthModel>;
|
||||||
|
if (!authenticators) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Object.values(authenticators);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setCache(authenticators: AuthModel[]) {
|
||||||
|
const obj = authenticators.reduce((obj, authenticator) => {
|
||||||
|
obj[authenticator.name] = authenticator;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
await this.cache.set(this.key, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(name: string) {
|
||||||
|
let authenticators = await this.getCache();
|
||||||
|
if (!authenticators.length) {
|
||||||
|
const repo = this.db.getRepository('authenticators');
|
||||||
|
authenticators = await repo.find({ filter: { enabled: true } });
|
||||||
|
await this.setCache(authenticators);
|
||||||
|
}
|
||||||
|
const authenticator = authenticators.find((authenticator: Model) => authenticator.name === name);
|
||||||
|
return authenticator || authenticators[0];
|
||||||
|
}
|
||||||
|
}
|
@ -2,13 +2,33 @@ import { ITokenBlacklistService } from '@nocobase/auth';
|
|||||||
import { Repository } from '@nocobase/database';
|
import { Repository } from '@nocobase/database';
|
||||||
import { CronJob } from 'cron';
|
import { CronJob } from 'cron';
|
||||||
import AuthPlugin from './plugin';
|
import AuthPlugin from './plugin';
|
||||||
|
import { BloomFilter } from '@nocobase/cache';
|
||||||
|
|
||||||
export class TokenBlacklistService implements ITokenBlacklistService {
|
export class TokenBlacklistService implements ITokenBlacklistService {
|
||||||
repo: Repository;
|
repo: Repository;
|
||||||
cronJob: CronJob;
|
cronJob: CronJob;
|
||||||
|
bloomFilter: BloomFilter;
|
||||||
|
cacheKey = 'token-black-list';
|
||||||
|
|
||||||
constructor(protected plugin: AuthPlugin) {
|
constructor(protected plugin: AuthPlugin) {
|
||||||
this.repo = plugin.db.getRepository('tokenBlacklist');
|
this.repo = plugin.db.getRepository('tokenBlacklist');
|
||||||
|
|
||||||
|
// Try to create a bloom filter and cache blocked tokens in it
|
||||||
|
plugin.app.on('beforeStart', async () => {
|
||||||
|
try {
|
||||||
|
this.bloomFilter = await plugin.app.cacheManager.createBloomFilter();
|
||||||
|
// https://redis.io/docs/data-types/probabilistic/bloom-filter/#reserving-bloom-filters
|
||||||
|
// 0.1% error rate requires 14.4 bits per item
|
||||||
|
// 14.4*1000000/8/1024/1024 = 1.72MB
|
||||||
|
await this.bloomFilter.reserve(this.cacheKey, 0.001, 1000000);
|
||||||
|
const data = await this.repo.find({ fields: ['token'], raw: true });
|
||||||
|
const tokens = data.map((item: any) => item.token);
|
||||||
|
await this.bloomFilter.mAdd(this.cacheKey, tokens);
|
||||||
|
} catch (error) {
|
||||||
|
plugin.app.logger.error('token-blacklist: create bloom filter failed', error);
|
||||||
|
this.bloomFilter = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get app() {
|
get app() {
|
||||||
@ -16,6 +36,12 @@ export class TokenBlacklistService implements ITokenBlacklistService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async has(token: string) {
|
async has(token: string) {
|
||||||
|
if (this.bloomFilter) {
|
||||||
|
const exists = await this.bloomFilter.exists(this.cacheKey, token);
|
||||||
|
if (!exists) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return !!(await this.repo.findOne({
|
return !!(await this.repo.findOne({
|
||||||
filter: {
|
filter: {
|
||||||
token,
|
token,
|
||||||
@ -25,10 +51,14 @@ export class TokenBlacklistService implements ITokenBlacklistService {
|
|||||||
|
|
||||||
async add(values) {
|
async add(values) {
|
||||||
await this.deleteExpiredTokens();
|
await this.deleteExpiredTokens();
|
||||||
|
const { token } = values;
|
||||||
|
if (this.bloomFilter) {
|
||||||
|
await this.bloomFilter.add(this.cacheKey, token);
|
||||||
|
}
|
||||||
return this.repo.model.findOrCreate({
|
return this.repo.model.findOrCreate({
|
||||||
defaults: values,
|
defaults: values,
|
||||||
where: {
|
where: {
|
||||||
token: values.token,
|
token,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -50,8 +50,8 @@ describe('createdBy/updatedBy', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = p2.toJSON();
|
const data = p2.toJSON();
|
||||||
expect(data.createdBy.id).toBe(currentUser.get('id'));
|
expect(data.createdBy.id).toBe(currentUser.id);
|
||||||
expect(data.updatedBy.id).toBe(currentUser.get('id'));
|
expect(data.updatedBy.id).toBe(currentUser.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('case 3', async () => {
|
it('case 3', async () => {
|
||||||
|
@ -56,12 +56,12 @@ export async function submit(context: Context, next) {
|
|||||||
}
|
}
|
||||||
const presetValues = processor.getParsedValue(actionItem.values ?? {}, userJob.nodeId, {
|
const presetValues = processor.getParsedValue(actionItem.values ?? {}, userJob.nodeId, {
|
||||||
// @deprecated
|
// @deprecated
|
||||||
currentUser: currentUser.toJSON(),
|
currentUser: currentUser,
|
||||||
// @deprecated
|
// @deprecated
|
||||||
currentRecord: values.result[formKey],
|
currentRecord: values.result[formKey],
|
||||||
// @deprecated
|
// @deprecated
|
||||||
currentTime: new Date(),
|
currentTime: new Date(),
|
||||||
$user: currentUser.toJSON(),
|
$user: currentUser,
|
||||||
$nForm: values.result[formKey],
|
$nForm: values.result[formKey],
|
||||||
$nDate: {
|
$nDate: {
|
||||||
now: new Date(),
|
now: new Date(),
|
||||||
|
47
yarn.lock
47
yarn.lock
@ -8124,6 +8124,11 @@ balanced-match@^1.0.0:
|
|||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||||
|
|
||||||
|
base64-arraybuffer@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
|
||||||
|
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
|
||||||
|
|
||||||
base64-js@^1.0.2, base64-js@^1.3.1:
|
base64-js@^1.0.2, base64-js@^1.3.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
resolved "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||||
@ -8199,6 +8204,21 @@ blessed@0.1.81:
|
|||||||
version "0.1.81"
|
version "0.1.81"
|
||||||
resolved "https://registry.npmmirror.com/blessed/-/blessed-0.1.81.tgz#f962d687ec2c369570ae71af843256e6d0ca1129"
|
resolved "https://registry.npmmirror.com/blessed/-/blessed-0.1.81.tgz#f962d687ec2c369570ae71af843256e6d0ca1129"
|
||||||
|
|
||||||
|
bloom-filters@^3.0.1:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.npmjs.org/bloom-filters/-/bloom-filters-3.0.1.tgz#13e28ed22febe2489cd00ba5bd98fdc90e820180"
|
||||||
|
integrity sha512-rU9IU6bgZ1jmqcLWhlKSidrFjbIGjB89CJBsQqUj1+3/11tAJDwn+f7iRu4bbQ2srTjGgNeoWNwcnelumqdi0g==
|
||||||
|
dependencies:
|
||||||
|
base64-arraybuffer "^1.0.2"
|
||||||
|
is-buffer "^2.0.5"
|
||||||
|
lodash "^4.17.15"
|
||||||
|
lodash.eq "^4.0.0"
|
||||||
|
lodash.indexof "^4.0.5"
|
||||||
|
long "^5.2.0"
|
||||||
|
reflect-metadata "^0.1.13"
|
||||||
|
seedrandom "^3.0.5"
|
||||||
|
xxhashjs "^0.2.2"
|
||||||
|
|
||||||
bluebird@^3.5.0, bluebird@^3.5.1:
|
bluebird@^3.5.0, bluebird@^3.5.1:
|
||||||
version "3.7.2"
|
version "3.7.2"
|
||||||
resolved "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
@ -9966,6 +9986,11 @@ csstype@^3.0.10, csstype@^3.0.2, csstype@^3.0.8:
|
|||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||||
|
|
||||||
|
cuint@^0.2.2:
|
||||||
|
version "0.2.2"
|
||||||
|
resolved "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
|
||||||
|
integrity sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==
|
||||||
|
|
||||||
culvert@^0.1.2:
|
culvert@^0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.npmmirror.com/culvert/-/culvert-0.1.2.tgz#9502f5f0154a2d5a22a023e79f71cc936fa6ef6f"
|
resolved "https://registry.npmmirror.com/culvert/-/culvert-0.1.2.tgz#9502f5f0154a2d5a22a023e79f71cc936fa6ef6f"
|
||||||
@ -16090,6 +16115,11 @@ lodash.difference@^4.5.0:
|
|||||||
version "4.5.0"
|
version "4.5.0"
|
||||||
resolved "https://registry.npmmirror.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c"
|
resolved "https://registry.npmmirror.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c"
|
||||||
|
|
||||||
|
lodash.eq@^4.0.0:
|
||||||
|
version "4.0.0"
|
||||||
|
resolved "https://registry.npmjs.org/lodash.eq/-/lodash.eq-4.0.0.tgz#a39f06779e72f9c0d1f310c90cd292c1661d5035"
|
||||||
|
integrity sha512-vbrJpXL6kQNG6TkInxX12DZRfuYVllSxhwYqjYB78g2zF3UI15nFO/0AgmZnZRnaQ38sZtjCiVjGr2rnKt4v0g==
|
||||||
|
|
||||||
lodash.flatten@^4.4.0:
|
lodash.flatten@^4.4.0:
|
||||||
version "4.4.0"
|
version "4.4.0"
|
||||||
resolved "https://registry.npmmirror.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
|
resolved "https://registry.npmmirror.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
|
||||||
@ -16102,6 +16132,11 @@ lodash.includes@^4.3.0:
|
|||||||
version "4.3.0"
|
version "4.3.0"
|
||||||
resolved "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
|
resolved "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
|
||||||
|
|
||||||
|
lodash.indexof@^4.0.5:
|
||||||
|
version "4.0.5"
|
||||||
|
resolved "https://registry.npmjs.org/lodash.indexof/-/lodash.indexof-4.0.5.tgz#53714adc2cddd6ed87638f893aa9b6c24e31ef3c"
|
||||||
|
integrity sha512-t9wLWMQsawdVmf6/IcAgVGqAJkNzYVcn4BHYZKTPW//l7N5Oq7Bq138BaVk19agcsPZePcidSgTTw4NqS1nUAw==
|
||||||
|
|
||||||
lodash.isboolean@^3.0.3:
|
lodash.isboolean@^3.0.3:
|
||||||
version "3.0.3"
|
version "3.0.3"
|
||||||
resolved "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
|
resolved "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
|
||||||
@ -20628,6 +20663,11 @@ redux@^4.0.0, redux@^4.0.4:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.9.2"
|
"@babel/runtime" "^7.9.2"
|
||||||
|
|
||||||
|
reflect-metadata@^0.1.13:
|
||||||
|
version "0.1.13"
|
||||||
|
resolved "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
|
||||||
|
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
|
||||||
|
|
||||||
reflect.getprototypeof@^1.0.2:
|
reflect.getprototypeof@^1.0.2:
|
||||||
version "1.0.3"
|
version "1.0.3"
|
||||||
resolved "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.3.tgz#2738fd896fcc3477ffbd4190b40c2458026b6928"
|
resolved "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.3.tgz#2738fd896fcc3477ffbd4190b40c2458026b6928"
|
||||||
@ -24346,6 +24386,13 @@ xtend@~2.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
object-keys "~0.4.0"
|
object-keys "~0.4.0"
|
||||||
|
|
||||||
|
xxhashjs@^0.2.2:
|
||||||
|
version "0.2.2"
|
||||||
|
resolved "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8"
|
||||||
|
integrity sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==
|
||||||
|
dependencies:
|
||||||
|
cuint "^0.2.2"
|
||||||
|
|
||||||
y18n@^3.2.1:
|
y18n@^3.2.1:
|
||||||
version "3.2.2"
|
version "3.2.2"
|
||||||
resolved "https://registry.npmmirror.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696"
|
resolved "https://registry.npmmirror.com/y18n/-/y18n-3.2.2.tgz#85c901bd6470ce71fc4bb723ad209b70f7f28696"
|
||||||
|
Loading…
Reference in New Issue
Block a user