Skip to content

deserializeOrganizationRole drops organization_id from event data, breaking Events API data sync #1484

@smorimoto

Description

@smorimoto

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_idorganizationId. 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 lost

Root 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:

  1. An organization_role.created event arrives for a role that does not yet exist locally.
  2. The deserialised data has no organizationId, so the consumer cannot determine which organisation the role belongs to.
  3. The context field for organization_role.* events is {} (empty), so it cannot be used as a fallback.
  4. The only remaining option is to scan all organisations and call listOrganizationRoles for 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions