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