Skip to content

Commit

Permalink
Merge pull request #26 from AplinkosMinisterija/populate-inherited-us…
Browse files Browse the repository at this point in the history
…er-apps-fix

Populate inherited user apps the right way
  • Loading branch information
ambrazasp authored Oct 17, 2024
2 parents 714ee67 + a9ef9be commit ee197ea
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 61 deletions.
128 changes: 128 additions & 0 deletions mixins/database.mixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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<any> {
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<number>, queryIds?: any) {
if (!queryIds) return ids;
Expand Down
28 changes: 19 additions & 9 deletions services/inheritedUserApps.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -41,6 +41,7 @@ export interface InheritedUserApp extends BaseModelInterface {
columnType: 'integer',
columnName: 'inheritedAppsIds',
populate: 'apps.resolve',
get: async ({ value }: FieldHookCallback) => value || [],
},
},
},
Expand All @@ -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({
Expand Down
20 changes: 10 additions & 10 deletions services/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> = 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',
});
},
},
},

Expand Down
6 changes: 1 addition & 5 deletions services/usersLocal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
});
Expand Down
68 changes: 31 additions & 37 deletions test/integration/api/users/users.groups.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand All @@ -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({
Expand All @@ -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({
Expand All @@ -125,34 +123,30 @@ 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 () => {
const groupHunters = {
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]);
});
});

0 comments on commit ee197ea

Please sign in to comment.