All files / realm/blong-access/adapter/db accessRoleEdit.ts

94.38% Statements 84/89
69.23% Branches 9/13
100% Functions 2/2
94.38% Lines 84/89

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 901x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 7x 7x 7x 7x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x   2x 3x         2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 7x 1x  
import {type IMeta, handler} from '@feasibleone/blong';
 
import * as model from './accessModel.ts';
 
type KnexQb = any;
 
/**
 * `access.role.edit` — update a role's columns, display name and record-level
 * ACL.
 *
 * The standard `access_role` update runs through `super.exec` (the virtual
 * `roleName` field is stripped — it lives in `core_resource`). The display name
 * is updated when submitted, and the role's scope-level `access_acl` rules are
 * brought in line with the submitted `matrix` (when present) — the tri-state
 * scope × CRUD grid of the Access tab, where an empty cell removes the rule and
 * therefore releases whatever the hierarchy granted implicitly.
 */
export default handler(
    ({
        errors,
        handler: {'db/coreResourceEnsure': coreResourceEnsure},
        lib: {ulid, crockfordDecode},
    }) => ({
        async accessRoleEdit(
            params: {
                role?: {
                    roleId?: string;
                    roleName?: string;
                    roleBit?: number | string | null;
                    description?: string;
                };
                matrix?: model.AclMatrixRow[];
            },
            $meta: IMeta,
        ): Promise<unknown> {
            const qb: KnexQb = this.config?.context?.queryBuilder;
            if (!qb) throw new Error('Database not available');
            const {role = {}, matrix} = params;
            const {roleName, ...roleColumns} = role;
            // A bit never moves: it is the role's position in the permission mask
            // of already-minted tokens, so re-pointing it would silently change
            // what those tokens authorize.  A form that posts the loaded bit is
            // fine; a different one is refused.
            const hex = model.binHex(role.roleId);
            if (!hex) throw new Error('Invalid role id');
            const submitted = role.roleBit;
            const blank = submitted === undefined || submitted === null || submitted === '';
            if (!blank) {
                const requested = Number(submitted);
                const stored = (await qb('access_role')
                    .where('roleId', Buffer.from(hex, 'hex'))
                    .first('roleBit')) as {roleBit?: number} | undefined;
                if (stored?.roleBit !== undefined && Number(stored.roleBit) !== requested) {
                    throw errors.roleBitImmutable({
                        params: {roleName: roleName ?? hex, roleBit: Number(stored.roleBit)},
                    });
                }
            }
            // The bit is not a writable column of the row: drop it either way
            // (the stored value is the authority).
            delete roleColumns.roleBit;
            // A matrix-only save submits the PK alone — there is nothing to
            // update on the row, and an empty `update()` is rejected by the
            // adapter.
            const touched = Object.keys(roleColumns).filter(key => key !== 'roleId');
            const result = touched.length
                ? await super.exec({...params, role: roleColumns}, $meta)
                : {role: {roleId: role.roleId}};
            if (typeof roleName === 'string') {
                await qb('core_resource')
                    .where('resourceId', Buffer.from(hex, 'hex'))
                    .update({resourceName: roleName});
            }
            if (Array.isArray(matrix)) {
                await model.syncAclMatrix(
                    qb,
                    {
                        coreResourceEnsure,
                        newAclId: () => Buffer.from(crockfordDecode(ulid())),
                    },
                    hex,
                    matrix,
                    $meta,
                );
            }
            return result;
        },
    }),
);