diff --git a/package-lock.json b/package-lock.json index 3d6742d..fc328fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2927,7 +2927,7 @@ }, "packages/core": { "name": "@relayfile/adapter-core", - "version": "0.1.4", + "version": "0.1.5", "license": "MIT", "dependencies": { "@scalar/postman-to-openapi": "^0.6.0", @@ -2954,7 +2954,7 @@ }, "packages/github": { "name": "@relayfile/adapter-github", - "version": "0.1.4", + "version": "0.1.5", "license": "MIT", "dependencies": { "@relayfile/adapter-core": "^0.1.1" @@ -2974,7 +2974,7 @@ }, "packages/gitlab": { "name": "@relayfile/adapter-gitlab", - "version": "0.1.4", + "version": "0.1.5", "license": "MIT", "dependencies": { "@relayfile/sdk": "^0.1.7" @@ -2990,7 +2990,7 @@ }, "packages/linear": { "name": "@relayfile/adapter-linear", - "version": "0.1.5", + "version": "0.1.6", "license": "MIT", "dependencies": { "@agent-relay/sdk": "^3.2.22", @@ -3007,7 +3007,7 @@ }, "packages/notion": { "name": "@relayfile/adapter-notion", - "version": "0.1.4", + "version": "0.1.5", "license": "MIT", "dependencies": { "@agent-relay/sdk": "^3.2.22", @@ -3022,7 +3022,7 @@ }, "packages/slack": { "name": "@relayfile/adapter-slack", - "version": "0.1.5", + "version": "0.1.6", "license": "MIT", "dependencies": { "@agent-relay/sdk": "^3.2.22", @@ -3039,7 +3039,7 @@ }, "packages/teams": { "name": "@relayfile/adapter-teams", - "version": "0.1.4", + "version": "0.1.5", "license": "MIT", "dependencies": { "@relayfile/sdk": "^0.1.7" diff --git a/packages/linear/src/__tests__/linear-adapter.test.ts b/packages/linear/src/__tests__/linear-adapter.test.ts index 5f237a0..88c5f95 100644 --- a/packages/linear/src/__tests__/linear-adapter.test.ts +++ b/packages/linear/src/__tests__/linear-adapter.test.ts @@ -10,7 +10,11 @@ import { linearCommentPath, linearCyclePath, linearIssuePath, + linearMilestonePath, linearProjectPath, + linearRoadmapPath, + linearTeamPath, + linearUserPath, normalizeLinearWebhook, validateLinearWebhookSignature, type ConnectionProvider, @@ -60,9 +64,15 @@ test('LinearAdapter exposes the provider name and supported Linear webhook event 'issue.create', 'issue.update', 'issue.remove', + 'milestone.create', + 'milestone.update', + 'milestone.remove', 'project.create', 'project.update', 'project.remove', + 'roadmap.create', + 'roadmap.update', + 'roadmap.remove', ]); }); @@ -207,23 +217,35 @@ test('signature rejection handling is deterministic for result and throwing help assert.deepEqual(missingSecret, { ok: false, reason: 'missing-secret' }); }); -test('path mapping stays deterministic for issue, comment, project, and cycle objects', () => { +test('path mapping stays deterministic for supported Linear VFS objects', () => { const adapter = createAdapter(); assert.equal(linearIssuePath('issue 1/2'), '/linear/issues/issue%201%2F2.json'); assert.equal(linearCommentPath('comment:42'), '/linear/comments/comment%3A42.json'); assert.equal(linearProjectPath('project#7'), '/linear/projects/project%237.json'); assert.equal(linearCyclePath('cycle Q2'), '/linear/cycles/cycle%20Q2.json'); + assert.equal(linearTeamPath('team eng'), '/linear/teams/team%20eng.json'); + assert.equal(linearUserPath('user@example.com'), '/linear/users/user%40example.com.json'); + assert.equal(linearMilestonePath('milestone/1'), '/linear/milestones/milestone%2F1.json'); + assert.equal(linearRoadmapPath('roadmap alpha'), '/linear/roadmaps/roadmap%20alpha.json'); assert.equal(computeLinearPath('Issue', 'issue 1/2'), '/linear/issues/issue%201%2F2.json'); assert.equal(computeLinearPath('comments', 'comment:42'), '/linear/comments/comment%3A42.json'); assert.equal(computeLinearPath('project', 'project#7'), '/linear/projects/project%237.json'); assert.equal(computeLinearPath('Cycles', 'cycle Q2'), '/linear/cycles/cycle%20Q2.json'); + assert.equal(computeLinearPath('teams', 'team eng'), '/linear/teams/team%20eng.json'); + assert.equal(computeLinearPath('users', 'user@example.com'), '/linear/users/user%40example.com.json'); + assert.equal(computeLinearPath('ProjectMilestone', 'milestone/1'), '/linear/milestones/milestone%2F1.json'); + assert.equal(computeLinearPath('roadmaps', 'roadmap alpha'), '/linear/roadmaps/roadmap%20alpha.json'); assert.equal(adapter.computePath('issues', 'issue 1/2'), '/linear/issues/issue%201%2F2.json'); assert.equal(adapter.computePath('comment', 'comment:42'), '/linear/comments/comment%3A42.json'); assert.equal(adapter.computePath('projects', 'project#7'), '/linear/projects/project%237.json'); assert.equal(adapter.computePath('cycle', 'cycle Q2'), '/linear/cycles/cycle%20Q2.json'); + assert.equal(adapter.computePath('team', 'team eng'), '/linear/teams/team%20eng.json'); + assert.equal(adapter.computePath('user', 'user@example.com'), '/linear/users/user%40example.com.json'); + assert.equal(adapter.computePath('milestone', 'milestone/1'), '/linear/milestones/milestone%2F1.json'); + assert.equal(adapter.computePath('roadmap', 'roadmap alpha'), '/linear/roadmaps/roadmap%20alpha.json'); }); test('computeSemantics extracts issue priority, state, labels, and relations deterministically', () => { @@ -322,10 +344,90 @@ test('computeSemantics extracts issue priority, state, labels, and relations det '/linear/issues/issue_parent.json', '/linear/issues/issue_related_z.json', '/linear/projects/project_alpha.json', + '/linear/teams/team_eng.json', ]); assert.equal(semantics.comments, undefined); }); +test('computeSemantics extracts synced Linear project, milestone, and roadmap relations', () => { + const adapter = createAdapter(); + + const projectSemantics = adapter.computeSemantics('LinearProject', 'project_alpha', { + id: 'project_alpha', + name: 'Alpha', + description: 'Top priority work', + team_ids: ['team_eng', 'team_platform'], + created_at: '2026-04-01T00:00:00.000Z', + updated_at: '2026-04-02T00:00:00.000Z', + }); + + assert.deepEqual(projectSemantics.properties, { + provider: 'linear', + 'provider.object_id': 'project_alpha', + 'provider.object_type': 'project', + 'linear.id': 'project_alpha', + 'linear.object_type': 'project', + 'linear.name': 'Alpha', + 'linear.description': 'Top priority work', + 'linear.created_at': '2026-04-01T00:00:00.000Z', + 'linear.updated_at': '2026-04-02T00:00:00.000Z', + 'linear.team_ids': 'team_eng, team_platform', + 'linear.team_count': '2', + }); + assert.deepEqual(projectSemantics.relations, [ + '/linear/teams/team_eng.json', + '/linear/teams/team_platform.json', + ]); + + const milestoneSemantics = adapter.computeSemantics('ProjectMilestone', 'milestone_beta', { + id: 'milestone_beta', + name: 'Beta', + status: 'planned', + progress: 0.4, + project_id: 'project_alpha', + project_name: 'Alpha', + }); + + assert.deepEqual(milestoneSemantics.properties, { + provider: 'linear', + 'provider.object_id': 'milestone_beta', + 'provider.object_type': 'milestone', + 'linear.id': 'milestone_beta', + 'linear.object_type': 'milestone', + 'linear.name': 'Beta', + 'linear.status': 'planned', + 'linear.progress': '0.4', + 'linear.project_id': 'project_alpha', + 'linear.project_name': 'Alpha', + }); + assert.deepEqual(milestoneSemantics.relations, ['/linear/projects/project_alpha.json']); + + const roadmapSemantics = adapter.computeSemantics('roadmap', 'roadmap_2026', { + id: 'roadmap_2026', + name: '2026 Roadmap', + project_ids: ['project_alpha', 'project_beta'], + team_ids: ['team_eng'], + }); + + assert.deepEqual(roadmapSemantics.properties, { + provider: 'linear', + 'provider.object_id': 'roadmap_2026', + 'provider.object_type': 'roadmap', + 'linear.id': 'roadmap_2026', + 'linear.object_type': 'roadmap', + 'linear.name': '2026 Roadmap', + 'linear.project_ids': 'project_alpha, project_beta', + 'linear.project_count': '2', + 'linear.team_ids': 'team_eng', + 'linear.team_count': '1', + }); + assert.deepEqual(roadmapSemantics.relations, [ + '/linear/projects/project_alpha.json', + '/linear/projects/project_beta.json', + '/linear/teams/team_eng.json', + ]); +}); + test('barrel exports import cleanly for runtime and type-checked usage', async () => { const barrel = await import('../index.js'); diff --git a/packages/linear/src/__tests__/types.test.ts b/packages/linear/src/__tests__/types.test.ts index fe5a51d..cc190c4 100644 --- a/packages/linear/src/__tests__/types.test.ts +++ b/packages/linear/src/__tests__/types.test.ts @@ -8,7 +8,14 @@ import { } from '../index.js'; test('exports supported Linear webhook object types', () => { - assert.deepEqual(LINEAR_WEBHOOK_OBJECT_TYPES, ['comment', 'cycle', 'issue', 'project']); + assert.deepEqual(LINEAR_WEBHOOK_OBJECT_TYPES, [ + 'comment', + 'cycle', + 'issue', + 'milestone', + 'project', + 'roadmap', + ]); }); test('exports supported Linear webhook actions', () => { diff --git a/packages/linear/src/linear-adapter.ts b/packages/linear/src/linear-adapter.ts index c27cf26..a67161a 100644 --- a/packages/linear/src/linear-adapter.ts +++ b/packages/linear/src/linear-adapter.ts @@ -5,18 +5,26 @@ import { computeLinearPath, linearCyclePath, linearIssuePath, + linearMilestonePath, linearProjectPath, + linearRoadmapPath, + linearTeamPath, + linearUserPath, normalizeLinearObjectType, } from './path-mapper.js'; +import { LINEAR_WEBHOOK_OBJECT_TYPES } from './types.js'; import type { LinearAdapterConfig, LinearComment, LinearCycle, LinearIssue, LinearLabel, + LinearMilestone, LinearProject, LinearRelation, + LinearRoadmap, LinearState, + LinearTeam, LinearUser, LinearWebhookPayload, } from './types.js'; @@ -103,7 +111,7 @@ type LinearRecord = Record; type LinearWebhookEnvelope = Record; const JSON_CONTENT_TYPE = 'application/json; charset=utf-8'; -const SUPPORTED_EVENTS = ['comment', 'cycle', 'issue', 'project'] as const; +const SUPPORTED_EVENTS = LINEAR_WEBHOOK_OBJECT_TYPES; const LINEAR_PROVIDER_NAME = 'linear'; export class LinearAdapter extends IntegrationAdapter { @@ -238,11 +246,23 @@ export class LinearAdapter extends IntegrationAdapter { applyCommentSemantics(properties, relations, comments, payload as LinearRecord); break; case 'project': - applyProjectSemantics(properties, payload as LinearRecord); + applyProjectSemantics(properties, relations, payload as LinearRecord); break; case 'cycle': applyCycleSemantics(properties, payload as LinearRecord); break; + case 'team': + applyTeamSemantics(properties, payload as LinearRecord); + break; + case 'user': + applyUserSemantics(properties, payload as LinearRecord); + break; + case 'milestone': + applyMilestoneSemantics(properties, relations, payload as LinearRecord); + break; + case 'roadmap': + applyRoadmapSemantics(properties, relations, payload as LinearRecord); + break; } const semantics: FileSemantics = { @@ -320,12 +340,12 @@ function applyIssueSemantics( addStringProperty(properties, 'linear.identifier', issue.identifier); addStringProperty(properties, 'linear.title', issue.title); - addStringProperty(properties, 'linear.branch_name', issue.branchName); - addStringProperty(properties, 'linear.due_date', issue.dueDate); - addStringProperty(properties, 'linear.created_at', issue.createdAt); - addStringProperty(properties, 'linear.updated_at', issue.updatedAt); - addStringProperty(properties, 'linear.completed_at', issue.completedAt); - addStringProperty(properties, 'linear.canceled_at', issue.canceledAt); + addFirstStringProperty(properties, 'linear.branch_name', issue.branchName, issue.branch_name); + addFirstStringProperty(properties, 'linear.due_date', issue.dueDate, issue.due_date); + addFirstStringProperty(properties, 'linear.created_at', issue.createdAt, issue.created_at); + addFirstStringProperty(properties, 'linear.updated_at', issue.updatedAt, issue.updated_at); + addFirstStringProperty(properties, 'linear.completed_at', issue.completedAt, issue.completed_at); + addFirstStringProperty(properties, 'linear.canceled_at', issue.canceledAt, issue.canceled_at); addNumberProperty(properties, 'linear.estimate', issue.estimate); const priority = asNumber(issue.priority); @@ -341,6 +361,8 @@ function applyIssueSemantics( addStringProperty(properties, 'linear.state_type', state.type); addStringProperty(properties, 'linear.state_color', state.color); } + addFirstStringProperty(properties, 'linear.state_name', properties['linear.state_name'], issue.state_name); + addFirstStringProperty(properties, 'linear.state_type', properties['linear.state_type'], issue.state_type); const assignee = issue.assignee as LinearUser | null | undefined; if (assignee) { @@ -349,6 +371,9 @@ function applyIssueSemantics( addStringProperty(properties, 'linear.assignee_email', assignee.email); addStringProperty(properties, 'linear.assignee_url', assignee.url); } + addFirstStringProperty(properties, 'linear.assignee_id', properties['linear.assignee_id'], issue.assignee_id); + addFirstStringProperty(properties, 'linear.assignee_name', properties['linear.assignee_name'], issue.assignee_name); + addFirstStringProperty(properties, 'linear.assignee_email', properties['linear.assignee_email'], issue.assignee_email); const creator = issue.creator as LinearUser | null | undefined; if (creator) { @@ -375,6 +400,12 @@ function applyIssueSemantics( addStringProperty(properties, 'linear.project_state', issue.project.state); addStringProperty(properties, 'linear.project_url', issue.project.url); } + const projectId = asString(issue.project_id); + if (projectId) { + relations.add(linearProjectPath(projectId)); + addStringProperty(properties, 'linear.project_id', projectId); + } + addFirstStringProperty(properties, 'linear.project_name', properties['linear.project_name'], issue.project_name); if (issue.cycle?.id) { relations.add(linearCyclePath(issue.cycle.id)); @@ -401,10 +432,18 @@ function applyIssueSemantics( } if (issue.team?.id) { + relations.add(linearTeamPath(issue.team.id)); addStringProperty(properties, 'linear.team_id', issue.team.id); addStringProperty(properties, 'linear.team_key', issue.team.key); addStringProperty(properties, 'linear.team_name', issue.team.name); } + const teamId = asString(issue.team_id); + if (teamId) { + relations.add(linearTeamPath(teamId)); + addStringProperty(properties, 'linear.team_id', teamId); + } + addFirstStringProperty(properties, 'linear.team_key', properties['linear.team_key'], issue.team_key); + addFirstStringProperty(properties, 'linear.team_name', properties['linear.team_name'], issue.team_name); } function applyCommentSemantics( @@ -415,16 +454,27 @@ function applyCommentSemantics( ): void { const comment = payload as Partial & LinearRecord; - addStringProperty(properties, 'linear.created_at', comment.createdAt); - addStringProperty(properties, 'linear.updated_at', comment.updatedAt); + addFirstStringProperty(properties, 'linear.created_at', comment.createdAt, comment.created_at); + addFirstStringProperty(properties, 'linear.updated_at', comment.updatedAt, comment.updated_at); const author = comment.user as LinearUser | null | undefined; if (author) { - addStringProperty(properties, 'linear.author_id', author.id); + const authorUserId = asString(author.id); + if (authorUserId) { + relations.add(linearUserPath(authorUserId)); + addStringProperty(properties, 'linear.author_id', authorUserId); + } addStringProperty(properties, 'linear.author_name', author.displayName ?? author.name); addStringProperty(properties, 'linear.author_email', author.email); addStringProperty(properties, 'linear.author_url', author.url); } + const authorId = asString(comment.user_id) ?? asString(comment.author_id); + if (authorId) { + relations.add(linearUserPath(authorId)); + } + addFirstStringProperty(properties, 'linear.author_id', properties['linear.author_id'], comment.user_id, comment.author_id); + addFirstStringProperty(properties, 'linear.author_name', properties['linear.author_name'], comment.user_name, comment.author_name); + addFirstStringProperty(properties, 'linear.author_email', properties['linear.author_email'], comment.user_email, comment.author_email); if (comment.issue?.id) { relations.add(linearIssuePath(comment.issue.id)); @@ -433,6 +483,14 @@ function applyCommentSemantics( addStringProperty(properties, 'linear.issue_title', comment.issue.title); addStringProperty(properties, 'linear.issue_url', comment.issue.url); } + const issueId = asString(comment.issue_id); + if (issueId) { + relations.add(linearIssuePath(issueId)); + addStringProperty(properties, 'linear.issue_id', issueId); + } + addFirstStringProperty(properties, 'linear.issue_identifier', properties['linear.issue_identifier'], comment.issue_identifier); + addFirstStringProperty(properties, 'linear.issue_title', properties['linear.issue_title'], comment.issue_title); + addFirstStringProperty(properties, 'linear.issue_url', properties['linear.issue_url'], comment.issue_url); const body = asString(comment.body); if (body) { @@ -441,19 +499,38 @@ function applyCommentSemantics( } } -function applyProjectSemantics(properties: Record, payload: LinearRecord): void { +function applyProjectSemantics( + properties: Record, + relations: Set, + payload: LinearRecord +): void { const project = payload as Partial & LinearRecord; addStringProperty(properties, 'linear.name', project.name); addStringProperty(properties, 'linear.state', project.state); - addStringProperty(properties, 'linear.target_date', project.targetDate); - addStringProperty(properties, 'linear.started_at', project.startedAt); - addStringProperty(properties, 'linear.completed_at', project.completedAt); + addFirstStringProperty(properties, 'linear.description', project.description); + addFirstStringProperty(properties, 'linear.target_date', project.targetDate, project.target_date); + addFirstStringProperty(properties, 'linear.started_at', project.startedAt, project.started_at); + addFirstStringProperty(properties, 'linear.completed_at', project.completedAt, project.completed_at); + addFirstStringProperty(properties, 'linear.created_at', project.createdAt, project.created_at); + addFirstStringProperty(properties, 'linear.updated_at', project.updatedAt, project.updated_at); const progress = asNumber(project.progress); if (progress !== undefined) { properties['linear.progress'] = String(progress); } + + const teamIds = uniqueStrings([ + ...asStringArray(project.team_ids), + ...asLinearReferenceIds(project.teams), + ]); + if (teamIds.length > 0) { + properties['linear.team_ids'] = teamIds.join(', '); + properties['linear.team_count'] = String(teamIds.length); + for (const teamId of teamIds) { + relations.add(linearTeamPath(teamId)); + } + } } function applyCycleSemantics(properties: Record, payload: LinearRecord): void { @@ -466,6 +543,92 @@ function applyCycleSemantics(properties: Record, payload: Linear addStringProperty(properties, 'linear.completed_at', cycle.completedAt); } +function applyTeamSemantics(properties: Record, payload: LinearRecord): void { + const team = payload as Partial & LinearRecord; + + addStringProperty(properties, 'linear.name', team.name); + addStringProperty(properties, 'linear.key', team.key); + addFirstStringProperty(properties, 'linear.description', team.description); + addFirstStringProperty(properties, 'linear.created_at', team.createdAt, team.created_at); + addFirstStringProperty(properties, 'linear.updated_at', team.updatedAt, team.updated_at); +} + +function applyUserSemantics(properties: Record, payload: LinearRecord): void { + const user = payload as Partial & LinearRecord; + + addStringProperty(properties, 'linear.name', user.name); + addFirstStringProperty(properties, 'linear.display_name', user.displayName, user.display_name); + addFirstStringProperty(properties, 'linear.first_name', user.firstName, user.first_name); + addFirstStringProperty(properties, 'linear.last_name', user.lastName, user.last_name); + addStringProperty(properties, 'linear.email', user.email); + addBooleanProperty(properties, 'linear.admin', user.admin); + addFirstStringProperty(properties, 'linear.avatar_url', user.avatarUrl, user.avatar_url); + addFirstStringProperty(properties, 'linear.updated_at', user.updatedAt, user.updated_at); +} + +function applyMilestoneSemantics( + properties: Record, + relations: Set, + payload: LinearRecord +): void { + const milestone = payload as Partial & LinearRecord; + + addStringProperty(properties, 'linear.name', milestone.name); + addStringProperty(properties, 'linear.status', milestone.status); + addFirstStringProperty(properties, 'linear.description', milestone.description); + addFirstStringProperty(properties, 'linear.created_at', milestone.createdAt, milestone.created_at); + addFirstStringProperty(properties, 'linear.updated_at', milestone.updatedAt, milestone.updated_at); + + const progress = asNumber(milestone.progress); + if (progress !== undefined) { + properties['linear.progress'] = String(progress); + } + + const projectId = asString(milestone.project?.id) ?? asString(milestone.project_id); + if (projectId) { + relations.add(linearProjectPath(projectId)); + addStringProperty(properties, 'linear.project_id', projectId); + } + addFirstStringProperty(properties, 'linear.project_name', milestone.project?.name, milestone.project_name); +} + +function applyRoadmapSemantics( + properties: Record, + relations: Set, + payload: LinearRecord +): void { + const roadmap = payload as Partial & LinearRecord; + + addStringProperty(properties, 'linear.name', roadmap.name); + addFirstStringProperty(properties, 'linear.description', roadmap.description); + addFirstStringProperty(properties, 'linear.created_at', roadmap.createdAt, roadmap.created_at); + addFirstStringProperty(properties, 'linear.updated_at', roadmap.updatedAt, roadmap.updated_at); + + const projectIds = uniqueStrings([ + ...asStringArray(roadmap.project_ids), + ...asLinearReferenceIds(roadmap.projects), + ]); + if (projectIds.length > 0) { + properties['linear.project_ids'] = projectIds.join(', '); + properties['linear.project_count'] = String(projectIds.length); + for (const projectId of projectIds) { + relations.add(linearProjectPath(projectId)); + } + } + + const teamIds = uniqueStrings([ + ...asStringArray(roadmap.team_ids), + ...asLinearReferenceIds(roadmap.teams), + ]); + if (teamIds.length > 0) { + properties['linear.team_ids'] = teamIds.join(', '); + properties['linear.team_count'] = String(teamIds.length); + for (const teamId of teamIds) { + relations.add(linearTeamPath(teamId)); + } + } +} + function asLabels(labels: LinearIssue['labels']): LinearLabel[] { return Array.isArray(labels) ? labels.filter((label): label is LinearLabel => Boolean(label?.name)) : []; } @@ -571,6 +734,20 @@ function addStringProperty(properties: Record, key: string, valu } } +function addFirstStringProperty( + properties: Record, + key: string, + ...values: unknown[] +): void { + for (const value of values) { + const normalized = asString(value); + if (normalized) { + properties[key] = normalized; + return; + } + } +} + function addNumberProperty(properties: Record, key: string, value: unknown): void { const normalized = asNumber(value); if (normalized !== undefined) { @@ -578,6 +755,12 @@ function addNumberProperty(properties: Record, key: string, valu } } +function addBooleanProperty(properties: Record, key: string, value: unknown): void { + if (typeof value === 'boolean') { + properties[key] = String(value); + } +} + function compactSemantics(semantics: FileSemantics): FileSemantics { const compacted: FileSemantics = {}; @@ -655,6 +838,31 @@ function asNumber(value: unknown): number | undefined { return undefined; } +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + + return value + .map((entry) => asString(entry)) + .filter((entry): entry is string => entry !== undefined); +} + +function asLinearReferenceIds(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + + return value + .map((entry) => getRecord(entry)) + .map((entry) => asString(entry?.id)) + .filter((entry): entry is string => entry !== undefined); +} + +function uniqueStrings(values: string[]): string[] { + return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right)); +} + function mapPriorityLabel(priority: number): string { switch (priority) { case 0: diff --git a/packages/linear/src/path-mapper.ts b/packages/linear/src/path-mapper.ts index 137c8ee..c429449 100644 --- a/packages/linear/src/path-mapper.ts +++ b/packages/linear/src/path-mapper.ts @@ -1,6 +1,15 @@ export const LINEAR_PATH_ROOT = '/linear'; -export const LINEAR_OBJECT_TYPES = ['comment', 'cycle', 'issue', 'project'] as const; +export const LINEAR_OBJECT_TYPES = [ + 'comment', + 'cycle', + 'issue', + 'milestone', + 'project', + 'roadmap', + 'team', + 'user', +] as const; export type LinearPathObjectType = (typeof LINEAR_OBJECT_TYPES)[number]; @@ -11,8 +20,26 @@ const OBJECT_TYPE_ALIASES: Readonly> = { cycles: 'cycle', issue: 'issue', issues: 'issue', + linearcomment: 'comment', + linearcycle: 'cycle', + linearissue: 'issue', + linearmilestone: 'milestone', + linearproject: 'project', + linearroadmap: 'roadmap', + linearteam: 'team', + linearuser: 'user', + milestone: 'milestone', + milestones: 'milestone', project: 'project', + projectmilestone: 'milestone', + projectmilestones: 'milestone', projects: 'project', + roadmap: 'roadmap', + roadmaps: 'roadmap', + team: 'team', + teams: 'team', + user: 'user', + users: 'user', }; function assertNonEmptySegment(value: string, label: string): string { @@ -52,6 +79,22 @@ export function linearCyclePath(cycleId: string): string { return `${LINEAR_PATH_ROOT}/cycles/${encodeLinearPathSegment(cycleId)}.json`; } +export function linearTeamPath(teamId: string): string { + return `${LINEAR_PATH_ROOT}/teams/${encodeLinearPathSegment(teamId)}.json`; +} + +export function linearUserPath(userId: string): string { + return `${LINEAR_PATH_ROOT}/users/${encodeLinearPathSegment(userId)}.json`; +} + +export function linearMilestonePath(milestoneId: string): string { + return `${LINEAR_PATH_ROOT}/milestones/${encodeLinearPathSegment(milestoneId)}.json`; +} + +export function linearRoadmapPath(roadmapId: string): string { + return `${LINEAR_PATH_ROOT}/roadmaps/${encodeLinearPathSegment(roadmapId)}.json`; +} + export function computeLinearPath(objectType: string, objectId: string): string { const normalizedType = normalizeLinearObjectType(objectType); const normalizedId = assertNonEmptySegment(objectId, 'object id'); @@ -65,5 +108,13 @@ export function computeLinearPath(objectType: string, objectId: string): string return linearProjectPath(normalizedId); case 'cycle': return linearCyclePath(normalizedId); + case 'team': + return linearTeamPath(normalizedId); + case 'user': + return linearUserPath(normalizedId); + case 'milestone': + return linearMilestonePath(normalizedId); + case 'roadmap': + return linearRoadmapPath(normalizedId); } } diff --git a/packages/linear/src/types.ts b/packages/linear/src/types.ts index 6dbca32..de82dfc 100644 --- a/packages/linear/src/types.ts +++ b/packages/linear/src/types.ts @@ -1,4 +1,11 @@ -export const LINEAR_WEBHOOK_OBJECT_TYPES = ['comment', 'cycle', 'issue', 'project'] as const; +export const LINEAR_WEBHOOK_OBJECT_TYPES = [ + 'comment', + 'cycle', + 'issue', + 'milestone', + 'project', + 'roadmap', +] as const; export const LINEAR_WEBHOOK_ACTIONS = ['create', 'remove', 'update'] as const; export type LinearWebhookObjectType = (typeof LINEAR_WEBHOOK_OBJECT_TYPES)[number]; @@ -22,8 +29,17 @@ export interface LinearUser { id: string; name?: string; displayName?: string; + display_name?: string; + firstName?: string; + first_name?: string; + lastName?: string; + last_name?: string; email?: string; + admin?: boolean; avatarUrl?: string; + avatar_url?: string; + updatedAt?: string; + updated_at?: string; url?: string; } @@ -31,6 +47,11 @@ export interface LinearTeam { id: string; key?: string; name?: string; + description?: string | null; + createdAt?: string; + created_at?: string; + updatedAt?: string; + updated_at?: string; } export interface LinearState { @@ -79,11 +100,49 @@ export interface LinearProject { state?: string; progress?: number | null; targetDate?: string | null; + target_date?: string | null; startedAt?: string | null; + started_at?: string | null; completedAt?: string | null; + completed_at?: string | null; + createdAt?: string; + created_at?: string; + updatedAt?: string; + updated_at?: string; + team_ids?: string[]; + teams?: LinearTeam[]; url?: string; } +export interface LinearMilestone { + id: string; + name?: string; + description?: string | null; + status?: string; + progress?: number | null; + project?: LinearProjectReference | null; + project_id?: string | null; + project_name?: string | null; + createdAt?: string; + created_at?: string; + updatedAt?: string; + updated_at?: string; +} + +export interface LinearRoadmap { + id: string; + name?: string; + description?: string | null; + project_ids?: string[]; + projects?: LinearProjectReference[]; + team_ids?: string[]; + teams?: LinearTeam[]; + createdAt?: string; + created_at?: string; + updatedAt?: string; + updated_at?: string; +} + export interface LinearCycle { id: string; number?: number; @@ -155,10 +214,14 @@ export type LinearIssueWebhookPayload = LinearWebhookBase; export type LinearCommentWebhookPayload = LinearWebhookBase; export type LinearProjectWebhookPayload = LinearWebhookBase; export type LinearCycleWebhookPayload = LinearWebhookBase; +export type LinearMilestoneWebhookPayload = LinearWebhookBase; +export type LinearRoadmapWebhookPayload = LinearWebhookBase; export type LinearWebhookPayload = | LinearCommentWebhookPayload | LinearCycleWebhookPayload | LinearIssueWebhookPayload + | LinearMilestoneWebhookPayload | LinearProjectWebhookPayload + | LinearRoadmapWebhookPayload | LinearWebhookBase; diff --git a/packages/linear/src/webhook-normalizer.ts b/packages/linear/src/webhook-normalizer.ts index c8ffb71..61dbd49 100644 --- a/packages/linear/src/webhook-normalizer.ts +++ b/packages/linear/src/webhook-normalizer.ts @@ -45,10 +45,18 @@ const OBJECT_TYPE_ALIASES: Readonly> = { issues: 'issue', label: 'label', labels: 'label', + milestone: 'milestone', + milestones: 'milestone', project: 'project', + projectmilestone: 'milestone', + projectmilestones: 'milestone', projects: 'project', reaction: 'reaction', reactions: 'reaction', + roadmap: 'roadmap', + roadmaps: 'roadmap', + team: 'team', + teams: 'team', user: 'user', users: 'user', };