From a9ef9be178346f9ae9ca0c5dac063c6e2b2bf91c Mon Sep 17 00:00:00 2001 From: Pranciskus Ambrazas Date: Thu, 17 Oct 2024 11:03:01 +0300 Subject: [PATCH] updates --- mixins/database.mixin.ts | 128 ++++++++++++++++++ services/inheritedUserApps.service.ts | 28 ++-- services/users.service.ts | 20 +-- services/usersLocal.service.ts | 6 +- .../api/users/users.groups.spec.ts | 68 +++++----- 5 files changed, 189 insertions(+), 61 deletions(-) diff --git a/mixins/database.mixin.ts b/mixins/database.mixin.ts index 2d6071d..b462dd1 100644 --- a/mixins/database.mixin.ts +++ b/mixins/database.mixin.ts @@ -4,6 +4,71 @@ import _ from 'lodash'; const DbService = require('@moleculer/database').Service; import config from '../knexfile'; import filtersMixin from 'moleculer-knex-filters'; +import { Context } from 'moleculer'; + +export function PopulateHandlerFn(action: string) { + return async function ( + ctx: Context<{ populate: string | string[] }>, + values: any[], + docs: any[], + field: any, + ) { + if (!values.length) return null; + const rule = field.populate; + let populate = rule.params?.populate; + if (rule.inheritPopulate) { + populate = ctx.params.populate; + } + const params = { + ...(rule.params || {}), + id: values, + mapping: true, + populate, + throwIfNotExist: false, + }; + + const byKey: any = await ctx.call(action, params, rule.callOptions); + + let fieldName = field.name; + if (rule.keyField) { + fieldName = rule.keyField; + } + + return docs?.map((d) => { + const fieldValue = d[fieldName]; + if (!fieldValue) return null; + return byKey[fieldValue] || null; + }); + }; +} + +function makeMapping( + data: any[], + mapping?: string, + options?: { + mappingMulti?: boolean; + mappingField?: string; + }, +) { + if (!mapping) return data; + + return data?.reduce((acc: any, item) => { + let value: any = item; + + if (options?.mappingField) { + value = item[options.mappingField]; + } + + if (options?.mappingMulti) { + return { + ...acc, + [`${item[mapping]}`]: [...(acc[`${item[mapping]}`] || []), value], + }; + } + + return { ...acc, [`${item[mapping]}`]: value }; + }, {}); +} export default function (opts: any = {}) { const adapter: any = { @@ -46,10 +111,73 @@ export default function (opts: any = {}) { async removeAllEntities(ctx: any) { return await this.clearEntities(ctx); }, + + async populateByProp( + ctx: Context<{ + id: number | number[]; + queryKey: string; + query: any; + mapping?: boolean; + mappingMulti?: boolean; + mappingField: string; + }>, + ): Promise { + const { queryKey, query, mapping, mappingMulti, mappingField } = ctx.params; + + const ids = Array.isArray(ctx.params.id) ? ctx.params.id : [ctx.params.id]; + + delete ctx.params.queryKey; + delete ctx.params.id; + delete ctx.params.mapping; + delete ctx.params.mappingMulti; + delete ctx.params.mappingField; + + const entities = await this.findEntities(ctx, { + ...ctx.params, + query: { + ...(query || {}), + [queryKey]: { $in: ids }, + }, + }); + + const resultById = makeMapping(entities, mapping ? queryKey : '', { + mappingMulti, + mappingField: mappingField, + }); + + return ids.reduce( + (acc: any, id) => ({ + ...acc, + [`${id}`]: resultById[id] || (mappingMulti ? [] : ''), + }), + {}, + ); + }, }, events, + hooks: { + after: { + find: [ + function ( + ctx: Context<{ + mapping: string; + mappingMulti: boolean; + mappingField: string; + }>, + data: any[], + ) { + const { mapping, mappingMulti, mappingField } = ctx.params; + return makeMapping(data, mapping, { + mappingMulti, + mappingField, + }); + }, + ], + }, + }, + methods: { filterQueryIds(ids: Array, queryIds?: any) { if (!queryIds) return ids; diff --git a/services/inheritedUserApps.service.ts b/services/inheritedUserApps.service.ts index 5b19df8..ce08e81 100644 --- a/services/inheritedUserApps.service.ts +++ b/services/inheritedUserApps.service.ts @@ -4,7 +4,7 @@ import moleculer, { Context } from 'moleculer'; import { Action, Service } from 'moleculer-decorators'; import DbConnection from '../mixins/database.mixin'; -import { BaseModelInterface } from '../types'; +import { BaseModelInterface, FieldHookCallback } from '../types'; import { App } from './apps.service'; import { User, UserType } from './users.service'; export interface InheritedUserApp extends BaseModelInterface { @@ -41,6 +41,7 @@ export interface InheritedUserApp extends BaseModelInterface { columnType: 'integer', columnName: 'inheritedAppsIds', populate: 'apps.resolve', + get: async ({ value }: FieldHookCallback) => value || [], }, }, }, @@ -58,20 +59,29 @@ export interface InheritedUserApp extends BaseModelInterface { export default class InheritedUserAppsService extends moleculer.Service { @Action({ params: { - user: { - type: 'number', - convert: true, - }, + user: [{ type: 'array', items: 'number|convert' }, 'number|convert'], + populate: 'string|optional', }, }) - async getAppsByUser(ctx: Context<{ user: number }>) { - const userWithApps: InheritedUserApp = await ctx.call('inheritedUserApps.findOne', { + async getAppsByUser(ctx: Context<{ user: number | number[]; populate: 'string' }>) { + const ids = ctx.params.user; + const multi = Array.isArray(ids); + + const { populate } = ctx.params; + const userWithApps: { [key: string]: App['id'][] } = await ctx.call('inheritedUserApps.find', { query: { - user: ctx.params.user, + user: multi ? { $in: ids } : ids, }, + mapping: 'user', + mappingField: 'inheritedApps', + populate, }); - return userWithApps?.inheritedApps || []; + if (!multi) { + return userWithApps[ids] || []; + } + + return userWithApps; } @Action({ diff --git a/services/users.service.ts b/services/users.service.ts index 5d490ae..631a07e 100644 --- a/services/users.service.ts +++ b/services/users.service.ts @@ -167,16 +167,16 @@ export interface User extends BaseModelInterface { type: 'array', virtual: true, items: { type: 'object' }, - populate(ctx: any, _values: any, items: any[]) { - return Promise.all( - items.map(async (item: any) => { - const appsIds: Array = await ctx.call('inheritedUserApps.getAppsByUser', { - user: item.id, - }); - if (!appsIds.length) return []; - return ctx.call('apps.resolve', { id: appsIds }); - }), - ); + populate: { + keyField: 'id', + async handler(ctx: any, userIds: number[], items: any[]) { + if (!userIds?.length) return; + + return ctx.call('inheritedUserApps.getAppsByUser', { + user: userIds, + populate: 'inheritedApps', + }); + }, }, }, diff --git a/services/usersLocal.service.ts b/services/usersLocal.service.ts index e067e88..08dbf87 100644 --- a/services/usersLocal.service.ts +++ b/services/usersLocal.service.ts @@ -393,14 +393,10 @@ export default class UsersLocalService extends moleculer.Service { >, ) { const { id, groups, password, oldPassword, email, unassignExistingGroups } = ctx.params; - let user: User = await ctx.call('users.resolve', { id }); + await ctx.call('users.resolve', { id, throwIfNotExist: true }); const { meta } = ctx; - if (!user) { - throwNotFoundError('User not found'); - } - const userLocal: UserLocal = await ctx.call('usersLocal.findOne', { query: { user: id }, }); diff --git a/test/integration/api/users/users.groups.spec.ts b/test/integration/api/users/users.groups.spec.ts index 51b6a2d..b571099 100644 --- a/test/integration/api/users/users.groups.spec.ts +++ b/test/integration/api/users/users.groups.spec.ts @@ -39,17 +39,21 @@ describe('Test assigning groups to user', () => { const groupToAssign1 = () => ({ id: apiHelper.groupAdminInner.id, role: UserGroupRole.ADMIN }); const groupToAssign2 = () => ({ id: apiHelper.groupAdminInner2.id, role: UserGroupRole.USER }); - afterEach(async () => { - await broker.call('usersLocal.updateUser', { - id: apiHelper.adminInner.id, - groups: [groupToAssign1()], - apps: [], - }); + afterEach((done) => { + new Promise(async (resolve) => { + await broker.call('usersLocal.updateUser', { + id: apiHelper.adminInner.id, + groups: [groupToAssign1()], + apps: [], + }); - await broker.call('usersLocal.updateUser', { - id: apiHelper.hunter.id, - groups: [], - apps: [apiHelper.appHunting.id], + await broker.call('usersLocal.updateUser', { + id: apiHelper.hunter.id, + groups: [], + apps: [apiHelper.appHunting.id], + }); + done(); + resolve(true); }); }); @@ -59,47 +63,41 @@ describe('Test assigning groups to user', () => { }; it('Assign groups for inner admin acting as admin (success)', async () => { - return request(apiService.server) + await request(apiService.server) .patch(`${endpoint}/${apiHelper.adminInner.id}`) .set(apiHelper.getHeaders(apiHelper.adminToken)) .send({ groups: groupsToAssign(), }) - .expect(200) - .expect(async (res: any) => { - await checkGroups(apiHelper.adminInner.id, groupsToAssign()); - }); + .expect(200); + await checkGroups(apiHelper.adminInner.id, groupsToAssign()); }); it('Assign groups (with unassign) for inner admin acting as admin (success)', async () => { - return request(apiService.server) + await request(apiService.server) .patch(`${endpoint}/${apiHelper.adminInner.id}`) .set(apiHelper.getHeaders(apiHelper.adminToken)) .send({ groups: [groupToAssign2()], }) - .expect(200) - .expect(async (res: any) => { - await checkGroups(apiHelper.adminInner.id, [groupToAssign2()]); - }); + .expect(200); + await checkGroups(apiHelper.adminInner.id, [groupToAssign2()]); }); it('Assign groups (without unassign) for inner admin acting as admin (success)', async () => { - return request(apiService.server) + await request(apiService.server) .patch(`${endpoint}/${apiHelper.adminInner.id}`) .set(apiHelper.getHeaders(apiHelper.adminToken)) .send({ groups: [groupToAssign2()], unassignExistingGroups: false, }) - .expect(200) - .expect(async (res: any) => { - await checkGroups(apiHelper.adminInner.id, [groupToAssign1(), groupToAssign2()]); - }); + .expect(200); + await checkGroups(apiHelper.adminInner.id, [groupToAssign1(), groupToAssign2()]); }); it('Assign groups for inner admin acting as inner admin (unauthorized)', async () => { - return request(apiService.server) + await request(apiService.server) .patch(`${endpoint}/${apiHelper.adminInner.id}`) .set(apiHelper.getHeaders(apiHelper.adminInnerToken)) .send({ @@ -112,7 +110,7 @@ describe('Test assigning groups to user', () => { }); it('Unassign groups for inner admin acting as admin (bad request - no apps & groups)', async () => { - return request(apiService.server) + await request(apiService.server) .patch(`${endpoint}/${apiHelper.adminInner.id}`) .set(apiHelper.getHeaders(apiHelper.adminToken)) .send({ @@ -125,17 +123,15 @@ describe('Test assigning groups to user', () => { }); it('Unassign groups for inner admin acting as admin (success)', async () => { - return request(apiService.server) + await request(apiService.server) .patch(`${endpoint}/${apiHelper.adminInner.id}`) .set(apiHelper.getHeaders(apiHelper.adminToken)) .send({ groups: [], apps: [apiHelper.appAdmin.id], }) - .expect(200) - .expect(async (res: any) => { - await checkGroups(apiHelper.adminInner.id, []); - }); + .expect(200); + await checkGroups(apiHelper.adminInner.id, []); }); it('Unassign apps for hunter and assigning group acting as admin (success)', async () => { @@ -143,16 +139,14 @@ describe('Test assigning groups to user', () => { id: apiHelper.groupHunters.id, role: UserGroupRole.USER, }; - return request(apiService.server) + await request(apiService.server) .patch(`${endpoint}/${apiHelper.hunter.id}`) .set(apiHelper.getHeaders(apiHelper.superAdminToken, apiHelper.appHunting.apiKey)) .send({ groups: [groupHunters], apps: [], }) - .expect(200) - .expect(async (res: any) => { - await checkGroups(apiHelper.hunter.id, [groupHunters]); - }); + .expect(200); + await checkGroups(apiHelper.hunter.id, [groupHunters]); }); });