-
Notifications
You must be signed in to change notification settings - Fork 51
Description
Summary
The deserializeOrganizationRole serialiser drops the organization_id field from organization_role.* event data. This makes it impossible to determine which organisation an organisation role belongs to when consuming the Events API for data synchronisation.
All other organisation-scoped resource deserialisers (OrganizationMembership, Connection, Session, OrganizationDomain, Invitation, DirectoryUser) correctly preserve organization_id → organizationId. The OrganizationRole deserialiser is the sole exception.
Additionally, deserializeRoleEvent (used for role.* events) drops id, name, description, and type, which similarly prevents identifying individual roles when processing events.
Reproduction
import { deserializeOrganizationRole } from "@workos-inc/node/lib/authorization/serializers/organization-role.serializer.js";
// Raw API response includes organization_id
// (see https://workos.com/docs/reference/events — organization_role.created)
const raw = {
object: "organization_role",
id: "role_01ABC",
organization_id: "org_01XYZ",
slug: "admin",
name: "Admin",
description: "Administrator role",
resource_type_slug: "organization",
permissions: ["users:read"],
type: "OrganizationRole",
created_at: "2024-01-01T00:00:00.000Z",
updated_at: "2024-01-01T00:00:00.000Z",
};
const deserialized = deserializeOrganizationRole(raw);
console.log(deserialized.organizationId); // undefined — field is lostRoot cause
organization-role.serializer.ts constructs a new object literal and does not include organization_id:
const deserializeOrganizationRole = (role) => ({
object: role.object,
id: role.id,
name: role.name,
slug: role.slug,
description: role.description,
permissions: role.permissions,
type: "OrganizationRole",
createdAt: role.created_at,
updatedAt: role.updated_at,
// organization_id is missing here
});Comparison with other deserialisers
| Resource type | Deserialiser | organizationId preserved? |
|---|---|---|
OrganizationMembership |
deserializeOrganizationMembership |
Yes |
Connection |
deserializeConnection |
Yes |
Session |
deserializeSession |
Yes |
OrganizationDomain |
deserializeOrganizationDomain |
Yes |
Invitation |
deserializeInvitationEvent |
Yes |
DirectoryUser |
deserializeDirectoryUser |
Yes |
OrganizationRole |
deserializeOrganizationRole |
No |
Impact
When polling the Events API (via workos.events.listEvents()) for data synchronisation:
- An
organization_role.createdevent arrives for a role that does not yet exist locally. - The deserialised
datahas noorganizationId, so the consumer cannot determine which organisation the role belongs to. - The
contextfield fororganization_role.*events is{}(empty), so it cannot be used as a fallback. - The only remaining option is to scan all organisations and call
listOrganizationRolesfor each one — an O(N) operation that does not scale.
Suggested fix
1. OrganizationRole interface and deserialiser
Add organizationId to the OrganizationRole interface and map organization_id in the deserialiser:
interface OrganizationRole {
object: "role";
id: string;
organizationId: string; // ← add
name: string;
slug: string;
description: string | null;
permissions: string[];
type: "OrganizationRole";
createdAt: string;
updatedAt: string;
}const deserializeOrganizationRole = (role) => ({
object: role.object,
id: role.id,
organizationId: role.organization_id, // ← add
name: role.name,
slug: role.slug,
description: role.description,
permissions: role.permissions,
type: "OrganizationRole",
createdAt: role.created_at,
updatedAt: role.updated_at,
});2. RoleEvent interface and deserialiser
deserializeRoleEvent currently returns only slug, permissions, createdAt, and updatedAt. For Events API consumers, id, name, description, and type are also needed to identify and sync individual roles. Consider aligning it with the full Role / EnvironmentRole shape.
Environment
@workos-inc/node:8.3.1- Node.js / Bun runtime on Cloudflare Workers
- Use case: Events API polling for data synchronisation to PostgreSQL