Repository: directus/directus
File: api/src/cli/commands/roles/create.ts (TypeScript)

Code before:
import { getSchema } from '../../../utils/get-schema.js';
import { RolesService } from '../../../services/roles.js';
import { PoliciesService } from '../../../services/index.js';
import { AccessService } from '../../../services/index.js';
import getDatabase from '../../../database/index.js';
import { useLogger } from '../../../logger/index.js';

export default async function rolesCreate({ role: name, admin }: { role: string; admin: boolean }): Promise<void> {
	const database = getDatabase();
	const logger = useLogger();

	if (!name) {
		logger.error('Name is required');
		process.exit(1);
	}

	try {
		const schema = await getSchema();
		const rolesService = new RolesService({ schema: schema, knex: database });
		const policiesService = new PoliciesService({ schema: schema, knex: database });
		const accessService = new AccessService({ schema: schema, knex: database });

		const adminPolicyId = await policiesService.knex
			.select('id')
			.from('directus_policies')
			.where('admin_access', true)
			.first();

		if (admin && !adminPolicyId) {
			logger.error('Cannot create an admin role without an admin policy');
			database.destroy();
			process.exit(1);
		}

		const roleId = await rolesService.createOne({ name });

		if (admin) {
			await accessService.createOne({
				role: roleId,
				policy: adminPolicyId.id,
			});
		}

		process.stdout.write(`${String(roleId)}\n`);
		database.destroy();
		process.exit(0);
	} catch (err: any) {
		logger.error(err);

Review comment (bug):
We should use `readOne` from the service here, as there might be differences in how databases store the values under the hood (f.e. in SQLite a boolean is `0` / `1` and will fail with a "where `true`"). The Directus methods will type cast these differences away.

That being said, seeing you're trying to create a whole new role, should we instead opt to just create a new admin policy that's just for this role? 🤔 Might be less confusing than reusing one that might be named for a certain use case or group.

Code after:
import { getSchema } from '../../../utils/get-schema.js';
import { RolesService } from '../../../services/roles.js';
import { PoliciesService } from '../../../services/index.js';
import { AccessService } from '../../../services/index.js';
import getDatabase from '../../../database/index.js';
import { useLogger } from '../../../logger/index.js';

export default async function rolesCreate({
	role: name,
	admin,
	app,
}: {
	role: string;
	admin: boolean;
	app: boolean;
}): Promise<void> {
	const database = getDatabase();
	const logger = useLogger();

	if (!name) {
		logger.error('Name is required');
		process.exit(1);
	}

	try {
		const schema = await getSchema();
		const rolesService = new RolesService({ schema: schema, knex: database });
		const policiesService = new PoliciesService({ schema: schema, knex: database });
		const accessService = new AccessService({ schema: schema, knex: database });

		const adminPolicyId = await policiesService.createOne({
			name: `Policy for ${name}`,
			admin_access: admin,
			app_access: app,
			icon: 'supervised_user_circle',
		});

		const roleId = await rolesService.createOne({ name });

		await accessService.createOne({
			role: roleId,
			policy: adminPolicyId,
		});

		process.stdout.write(`${String(roleId)}\n`);
		database.destroy();
		process.exit(0);
	} catch (err: any) {