如何在MongoDB中植入角色和功能



我是MongoDB和Docker的新手,我正在开发一个应用程序,找不到一种更微妙的方法来使用npm run命令为我的数据库种子。首先,我创建了一个名为seed.js的文件,然后将其与package.json文件上的npm run seed命令相关联。

在seed.js文件中,我导入了Mongoose和模型,但我需要做的两件事是:

  • 如果角色还不存在,则创建

  • 创建功能(如果它们还不存在),并将其与角色

我想要创建的角色是:

  • 管理员(描述:管理员)

  • 查看器(描述:查看器)

功能

我需要检查用户服务的每个端点,这些端点应该需要身份验证并创建足够的功能。示例:updateUser更新用户数据。这可以由自己的用户(因此必须具有updateUserOwn功能)和管理员(将具有updateUsers功能)来完成。我必须分析每个端点,并判断什么是足够的,但我仍然无法找到将初始角色和功能引入数据库的方法。

UPDATE:在种子设定本身上,更新的解决方案是有效的,但它需要大量的代码和重复,这些代码和重复可能会通过循环来修复。我想首先开始创建角色,这意味着创建一个包含对象的数组,其中包含要创建的角色的数据。每个角色都有字段角色和描述

const userRole = [{
role: admin
description: Administrator
},
{
role: viewer
description: Viewer
}]

这个想法是,如果角色存在,就不需要更新,但我不知道如何在数组中循环,并仅在不存在的情况下创建角色。类似于使用updateOne,带有upsert:true选项,但带有$setOnInsert上的数据,因为只有在插入文档时才会添加数据。

我只需要创建而不需要更新,因为将来我将通过API直接编辑角色。因此,例如,如果对管理员角色进行了更改,种子将不会覆盖它

在循环过程中,我需要创建一个名为rolesId的关联数组,该数组将存储创建的角色的ObjectId。它应该会产生这样的结果:

[
"admin": "iaufh984whrfj203jref",
"viewer": "r9i23jfeow9iefd0ew0",
]

此外,每个能力都必须有一系列必须关联的角色。例如:

{
capability: "updateUsers",
description: "Update the data of all users",
roles: ["admin"]
}

如何循环遍历每个元素上的数组,准备使用带有对象ID的数组插入它。代替角色:["管理员"]?类似于角色:["iaufh984whrfj203jref"],否则将出现强制转换错误。请记住,每个功能可能与多个角色关联,所以我可能需要循环使用它们,但我找不到创建该逻辑的方法。

用户型号

const userSchema = new mongoose.Schema(
{
.......
role: {
ref: "roles",
type: mongoose.Schema.Types.ObjectId,
},
);
module.exports = mongoose.model("User", userSchema);

角色模型:

const roles = new mongoose.Schema({
role: {
type: String,
required: true,
},
capabilities: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "capabilities",
},
],
});
module.exports = mongoose.model("roles", roles);

能力模型:

const capabilities = new mongoose.Schema({
capability: {
type: String,
required: true,
},
name: {
type: String,
},
});
module.exports = mongoose.model("capabilities", capabilities);

更新:种子文件:

const seedDB = async () => {
if (!process.env.DB_URI) {
throw new Error("Error connecting to MongoDB: DB_URI is not defined.");
}
try {
await mongoose.connect(process.env.DB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
});
console.log("Connected to MongoDB");
const tasks = [
Capability.findOneAndUpdate(
{ name: "updateUserOwn" },
{ capability: "updateUser" },
{ upsert: true }
).exec(),
Capability.findOneAndUpdate(
{ name: "updateUsers" },
{ capability: "updateUser" },
{ upsert: true }
).exec(),
// Seed more...
];
const [updateUserOwn, updateUsers] = await Promise.all(tasks);
Role.bulkWrite([
{
updateOne: {
filter: { role: "Admin" },
update: { capabilities: [updateUsers] },
upsert: true,
},
},
{
updateOne: {
filter: { role: "Viewer" },
update: { capabilities: [updateUserOwn] },
upsert: true,
},
},
]);
console.log("seeded data", tasks);
} catch (error) {
console.log(`Error connecting to MongoDB: ${error}`);
}
};
seedDB();

您总体上走在了正确的道路上。

因为功能被用作引用,所以在将它们分配给角色之前,您必须获取或创建它们(获取引用)。

这可能是你的种子逻辑:

const tasks = [
Capability.findOneAndUpdate(
{ name: 'updateUserOwn' }, // matches or creates this capability
{ capability: 'updateUser' }, // adds this to the object
{ upsert: true, new: true } // `new` guarantees an object is always returned
}).exec(),
Capability.findOneAndUpdate(
{ name: 'updateUsers' },
{ capability: 'updateUser' },
{ upsert: true, new: true }
}).exec(),
// Seed more...
];
const [
updateUserOwn,
updateUsers,
] = await Promise.all(tasks);
// We can use bulk write for the second transaction so it runs in one go
await Role.bulkWrite([
{ 
updateOne: {
filter: { role: 'Admin' },
update: { capabilities: [updateUsers] },
upsert: true,
}
},
{ 
updateOne: {
filter: { role: 'Viewer' },
update: { capabilities: [updateUserOwn] },
upsert: true,
}
}
]);

我们使用findOneAndUpdate逐个播种功能,这样我们就可以获得我们打算在角色上使用的每个功能的参考

然后我们使用bulkWrite来播种角色

我可能已经交换了功能和它们的名称,但我希望你能大致了解


如果没有引用,种子会更简单-你可以一次性使用bulkWrite所有内容,但为了创建具有内部引用的对象或添加对此类对象的引用,你首先需要有实际的引用

您可以创建静态映射和循环,这将稍微减少代码,并使事情变得更容易。这也允许您跳过已经存在的种子项目

由于功能是通过角色重用的,我想先创建它们,但改变逻辑先创建角色,然后再创建功能是没有问题的,尽管这可能不是直接的

此外,每个功能都必须有一组必须关联的角色。

这被称为";多对多";关系(因为角色也有一系列对功能的引用),这只会使逻辑复杂化。你确定你真的需要它吗?猫鼬/mongoo不会自动为你管理它:

  • 当您向角色添加功能时,还需要同步并在capability.roles中添加角色-手动
  • 相反,在capability.roles中添加一个角色,您需要同步它,并手动将该功能添加到role.capabilities
  • 删除功能或角色也是如此-手动清理
  • 它可能会失败,需要恢复-例如,向role.capabilities中添加了一个功能,但由于某种原因,执行停止,角色没有添加到capability.roles中-因此整个处理可能需要封装在事务中
  • 存在交叉引用角色和能力而不必具有";多对多";关系

以下是一种使用save中间件同步多对多关系以创建/更新的简单方法

角色.js
const mongoose = require('mongoose');
const roles = new mongoose.Schema({
role: {
type: String,
required: true,
},
description: String,
capabilities: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'capabilities',
},
],
});
roles.pre('save', async function save() {
// Doesn't need to run if there are no capabilities
if (!this.capabilities || this.capabilities.length === 0) return;
const Capability = mongoose.model('capabilities');
await Capability.updateMany(
{ _id: {$in: this.capabilities} },
// Adds only if it's missing
{ $addToSet: { roles: this._id }},
);
});
// Todo: similar logic to remove from capabilities if role is deleted
module.exports = mongoose.model("roles", roles);
Capability.js
const mongoose = require('mongoose');
const capabilities = new mongoose.Schema({
capability: {
type: String,
required: true,
},
description: {
type: String,
},
roles: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'roles',
}
]
});
capabilities.pre('save', async function save() {
if (!this.roles || this.roles.length === 0) return;
const Role = mongoose.model('roles');
await Role.updateMany(
{_id: {$in: this.roles}},
{$addToSet: {capabilities: this._id}},
);
})
// Todo: similar logic to remove from roles if capability is deleted
module.exports = mongoose.model("capabilities", capabilities);

这里有一个更新种子例程:

种子.js
const mongoose = require('mongoose');
const Capability = require('./models/Capability');
const Role = require('./models/Role');
const CAPABILITIES = {
UPDATE_USERS: {
capability: 'updateUsers',
description: 'Update the data of all users',
},
VIEW_USERS: {
capability: 'viewUsers',
description: 'View public data of users',
},
UPDATE_OWN_RECORD: {
capability: 'updateUserOwn',
description: 'Update user own data',
}
}
const ROLES_TO_SEED = [
{
role: 'admin',
description: 'Administrator',
capabilities: [CAPABILITIES.UPDATE_USERS, CAPABILITIES.VIEW_USERS],
},
{
role: 'viewer',
description: 'Viewer',
capabilities: [CAPABILITIES.VIEW_USERS, CAPABILITIES.UPDATE_OWN_RECORD],
}
]
const seedDB = async () => {
await connectToDb();
await seedRoles();
};
const connectToDb = async () => {
if (!process.env.DB_URI) throw new Error('DB_URI is not defined.');
console.info('Connecting to database...');
await mongoose.connect(process.env.DB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false,
});
console.info('Connected n');
}
const seedRoles = async () => {
console.log('Seeding Roles...');
// runs sequentially to skip creating duplicate capabilities
for (const role of ROLES_TO_SEED) {
await findOrCreateRole(role);
}
console.log('Complete n');
}
const findOrCreateRole = async ({capabilities, role, ...defaults}) => {
console.info('Looking for role: ', role);
const fromDb = await Role.findOne({role}).exec();
if (fromDb) {
console.info('Role already exists skipping... n');
return fromDb;
}
console.info('Role does not exist - creating new n');
const doc = new Role({role, ...defaults});
// All capabilities (per role) can be created/found in parallel
const roleCapabilities = await Promise.all(capabilities.map(findOrCreateCapability));
doc.capabilities = roleCapabilities.map(c => c._id);
await doc.save();
console.info('Role created: ', role);
console.info('');
return doc;
}
const findOrCreateCapability = async ({capability, ...defaults}) => {
console.info('Looking for capability: ', capability);
let doc = await Capability.findOne({capability}).exec();
if (doc) {
console.info(`Capability ${capability} found - using existing...`);
}
else {
console.info(`Capability ${capability} does not exist - creating new`);
doc = new Capability({capability, ...defaults});
await doc.save();
}
return doc;
}
seedDB()
.then(() => {
console.info('Exiting...: ');
process.exit(0);
})
.catch(error => {
console.error('Seed failed');
console.error(error);
process.exit(1);
})

我们有一个功能字典和一个可以映射到数据库操作的角色列表。这个想法是,每个角色都应该包含一个能力的完整定义,它可以用来找到现有的能力,或者在不存在的情况下创建它

对于列表中的每个角色,我们都会进行查询,看看它是否存在。

  • 当它存在时,我们什么都不做,然后转到下一个角色
  • 当它不存在时,我们拥有创建它所需的所有数据,并创建/查找它可能需要的任何功能

当您计算出应用程序的所有角色和功能时,只需将它们放在roles_TO_SEED和capabilities静态映射中

该脚本依赖于上面提到的模型中的中间件修改


还有少量奖金

您不需要多对多的关系来将功能与所使用的角色相匹配。以下是如果只有角色模型具有一系列功能(参考文献),则可以聚合这些信息的方法。在数据库播种后运行此操作:

const showCapabilitiesUsages = async () => {
const result = await Capability.aggregate([
{
$lookup: {
from: 'roles',
let: {searched: '$_id'},
pipeline: [
{
$match: {
$expr: {
$in: ['$$searched', '$capabilities']
}
}
}
],
as: 'roles'
}
}, {
$project: {
_id: 0,
capability: 1,
description: 1,
usedInRoles: {
$map: {
input: '$roles',
as: 'role',
in: '$$role.role',
}
}
}
}
]).exec();
console.log('Aggregate result: ', result);
}

你应该得到这样的结果:

Aggregate result:  [
{
capability: 'updateUsers',
description: 'Update the data of all users',
usedInRoles: [ 'admin' ]
},
{
capability: 'viewUsers',
description: 'View public data of users',
usedInRoles: [ 'admin', 'viewer' ]
},
{
capability: 'updateUserOwn',
description: 'Update user own data',
usedInRoles: [ 'viewer' ]
}
]

试试这样的东西,它应该会起作用:

const roles = [
{
name: 'admin',
description: 'Administrator',
},
{
name: 'viewer',
description: 'Viewer',
},
];
const capabilities = [
// Capabilities
{
name: 'createCapability',
description: 'Create a new capability',
roles: ['admin'],
},
{
name: 'deleteCapability',
description: 'Delete a capability',
roles: ['admin'],
}
// Roles
{
name: 'createRole',
description: 'Create a new role',
roles: ['admin'],
},
{
name: 'deleteRole',
description: 'Delete a role',
roles: ['admin'],
},

// Users
{
name: 'updateUser',
description: 'Update current user data',
roles: ['viewer'],
},
{
name: 'updateUsers',
description: 'Update the data from any user',
roles: ['admin'],
},
];

const seedRoles = async (roles) => {
if (0 == roles.length || !Array.isArray(roles)) {
return;
}
console.log('');
for (const role of roles) {
const savedRole = await Role.findOneAndUpdate(
{name: role.name},
{$setOnInsert: role},
{upsert: true, new: true, rawResult: true},
);
if (!savedRole) {
console.log(`Role “${savedRole.value.name}” already on database.`);
} else {
console.log(`Role “${savedRole.value.name}” added to database.`);
}
}
};
const seedCapabilities = async (capabilities) => {
if (0 == capabilities.length || !Array.isArray(capabilities)) {
return;
}
console.log('');
for (const capability of capabilities) {
const rolesToPush = capability.roles;
delete capability.roles;
const addedCapability = await Capability.findOneAndUpdate(
{name: capability.name},
{$setOnInsert: capability},
{upsert: true, new: true, rawResult: true},
);
if (!addedCapability) {
console.log(
`Capability “${addedCapability.value.name}” ` +
`already on database.`,
);
} else {
console.log(
`Capability “${addedCapability.value.name}” ` +
`added to database.`,
);
if (rolesToPush && Array.isArray(rolesToPush)) {
rolesToPush.forEach(async (role) => {
const roleToPush = await Role.findOne({name: role});
if (roleToPush) {
roleToPush.capabilities.push(addedCapability.value);
await roleToPush.save();
}
});
}
}
}
};
const seedDb = async (roles, capabilities, users) => {
try {
await seedRoles(roles);
await seedCapabilities(capabilities);
console.log('roles', roles);
} catch (error) {
console.error(error);
}
};
module.exports = seedDb;

最新更新