From 7b3d7f0d1e939395180d38d705f785f80bd2e971 Mon Sep 17 00:00:00 2001 From: Patrick Heneise Date: Wed, 29 Oct 2025 10:38:20 -0600 Subject: [PATCH] feat: add organization statistics support (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add functionality to fetch organization-level statistics and metadata. ## Changes - Add `getOrganization()` function to fetch org metadata - Add GraphQL query for organization data - Add comprehensive test suite - Update README with API documentation ## API ```javascript import { getOrganization } from 'gitevents-fetch' const org = await getOrganization('myorg') // Returns: { name, login, description, memberCount, publicRepoCount, ... } ``` ## Features - Organization metadata (name, description, website, location) - Member count and public repository count - Avatar URL and contact email - Created/updated timestamps - Returns null if organization not found 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 36 +++++++++ src/graphql/organization.gql | 21 +++++ src/index.js | 5 ++ src/lib/parseGql.js | 4 +- src/organization.js | 46 +++++++++++ test/organization.test.js | 143 +++++++++++++++++++++++++++++++++++ 6 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 src/graphql/organization.gql create mode 100644 src/organization.js create mode 100644 test/organization.test.js diff --git a/README.md b/README.md index e81e9f0..d88a999 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A Node.js library for fetching events and talks from GitEvents-based GitHub repo - 🚀 Fetch upcoming and past events from GitHub Issues - 🎤 Retrieve event talks and speaker submissions (via sub-issues) +- 🏢 Fetch organization statistics and metadata - 👥 Fetch GitHub Teams and team members - 🔐 Support for both GitHub Personal Access Tokens (PAT) and GitHub App authentication - 📊 Parse structured event data using issue forms @@ -214,6 +215,41 @@ console.log(team) **Note:** Returns `null` if the team is not found. +### `getOrganization(org)` + +Fetch organization statistics and metadata. + +**Parameters:** + +- `org` (string) - GitHub organization name + +**Returns:** `Promise` + +Returns organization data or `null` if not found. + +**Example:** + +```javascript +import { getOrganization } from 'gitevents-fetch' + +const org = await getOrganization('myorg') + +console.log(org) +// { +// name: 'My Organization', +// login: 'myorg', +// description: 'We build amazing things', +// websiteUrl: 'https://myorg.com', +// avatarUrl: 'https://github.com/myorg.png', +// email: 'hello@myorg.com', +// location: 'San Francisco, CA', +// createdAt: Date('2020-01-01T00:00:00.000Z'), +// updatedAt: Date('2024-01-01T00:00:00.000Z'), +// memberCount: 42, +// publicRepoCount: 128 +// } +``` + ### Event Object Structure ```typescript diff --git a/src/graphql/organization.gql b/src/graphql/organization.gql new file mode 100644 index 0000000..24601c3 --- /dev/null +++ b/src/graphql/organization.gql @@ -0,0 +1,21 @@ +query ( + $organization: String! +) { + organization(login: $organization) { + name + login + description + websiteUrl + avatarUrl + email + location + createdAt + updatedAt + membersWithRole { + totalCount + } + repositories(privacy: PUBLIC) { + totalCount + } + } +} diff --git a/src/index.js b/src/index.js index da78b63..289acb0 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ import { graphql } from '@octokit/graphql' import { ghAppId, ghAppInstallationId, ghPrivateKey, ghPAT } from './config.js' import { listUpcomingEvents, listPastEvents, getEvent } from './events.js' import { getTeamById } from './teams.js' +import { getOrganization as getOrg } from './organization.js' function createAuth() { // Use PAT if provided (and no private key) @@ -55,3 +56,7 @@ export async function event(org, repo, number) { export async function getTeam(org, teamSlug) { return getTeamById(graphqlWithAuth, org, teamSlug) } + +export async function getOrganization(org) { + return getOrg(graphqlWithAuth, org) +} diff --git a/src/lib/parseGql.js b/src/lib/parseGql.js index eef47db..6163d6b 100644 --- a/src/lib/parseGql.js +++ b/src/lib/parseGql.js @@ -2,11 +2,13 @@ import { defaultApprovedEventLabel } from '../config.js' import eventsQuery from '../graphql/events.gql?raw' import eventQuery from '../graphql/event.gql?raw' import teamQuery from '../graphql/team.gql?raw' +import organizationQuery from '../graphql/organization.gql?raw' const queries = { events: eventsQuery, event: eventQuery, - team: teamQuery + team: teamQuery, + organization: organizationQuery } export async function parseGql(path) { diff --git a/src/organization.js b/src/organization.js new file mode 100644 index 0000000..fec7711 --- /dev/null +++ b/src/organization.js @@ -0,0 +1,46 @@ +import { parseGql } from './lib/parseGql.js' + +function validateParams(params) { + const missing = [] + for (const [key, value] of Object.entries(params)) { + if (!value) missing.push(key) + } + if (missing.length > 0) { + throw new Error(`Missing required parameters: ${missing.join(', ')}`) + } +} + +export async function getOrganization(graphql, org) { + validateParams({ graphql, org }) + + try { + const query = await parseGql('organization') + const vars = { + organization: org + } + + const result = await graphql(query, vars) + + if (!result.organization) { + return null + } + + const org = result.organization + + return { + name: org.name || null, + login: org.login || null, + description: org.description || null, + websiteUrl: org.websiteUrl || null, + avatarUrl: org.avatarUrl || null, + email: org.email || null, + location: org.location || null, + createdAt: org.createdAt ? new Date(org.createdAt) : null, + updatedAt: org.updatedAt ? new Date(org.updatedAt) : null, + memberCount: org.membersWithRole?.totalCount || 0, + publicRepoCount: org.repositories?.totalCount || 0 + } + } catch (error) { + throw new Error(`Failed to fetch organization: ${error.message}`) + } +} diff --git a/test/organization.test.js b/test/organization.test.js new file mode 100644 index 0000000..5c3fcea --- /dev/null +++ b/test/organization.test.js @@ -0,0 +1,143 @@ +import test from 'node:test' +import assert from 'node:assert' +import { getOrganization } from '../src/organization.js' + +test('getOrganization - validates required parameters', async () => { + await assert.rejects( + async () => { + await getOrganization(null, 'org') + }, + { + message: /Missing required parameters/ + }, + 'Should validate graphql parameter' + ) + + await assert.rejects( + async () => { + const mockGraphql = () => {} + await getOrganization(mockGraphql, null) + }, + { + message: /Missing required parameters/ + }, + 'Should validate org parameter' + ) +}) + +test('getOrganization - fetches organization successfully', async () => { + const mockGraphql = async () => ({ + organization: { + name: 'Test Organization', + login: 'test-org', + description: 'A test organization', + websiteUrl: 'https://test-org.com', + avatarUrl: 'https://github.com/test-org.png', + email: 'hello@test-org.com', + location: 'San Francisco, CA', + createdAt: '2020-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + membersWithRole: { + totalCount: 42 + }, + repositories: { + totalCount: 128 + } + } + }) + + const result = await getOrganization(mockGraphql, 'test-org') + + assert.equal(result.name, 'Test Organization') + assert.equal(result.login, 'test-org') + assert.equal(result.description, 'A test organization') + assert.equal(result.websiteUrl, 'https://test-org.com') + assert.equal(result.avatarUrl, 'https://github.com/test-org.png') + assert.equal(result.email, 'hello@test-org.com') + assert.equal(result.location, 'San Francisco, CA') + assert.ok(result.createdAt instanceof Date) + assert.ok(result.updatedAt instanceof Date) + assert.equal(result.memberCount, 42) + assert.equal(result.publicRepoCount, 128) +}) + +test('getOrganization - handles missing optional fields', async () => { + const mockGraphql = async () => ({ + organization: { + name: 'Test Org', + login: 'test-org', + description: null, + websiteUrl: null, + avatarUrl: 'https://github.com/test-org.png', + email: null, + location: null, + createdAt: '2020-01-01T00:00:00Z', + updatedAt: null, + membersWithRole: { + totalCount: 5 + }, + repositories: null + } + }) + + const result = await getOrganization(mockGraphql, 'test-org') + + assert.equal(result.description, null) + assert.equal(result.websiteUrl, null) + assert.equal(result.email, null) + assert.equal(result.location, null) + assert.equal(result.updatedAt, null) + assert.equal(result.publicRepoCount, 0) +}) + +test('getOrganization - returns null if organization not found', async () => { + const mockGraphql = async () => ({ + organization: null + }) + + const result = await getOrganization(mockGraphql, 'nonexistent-org') + + assert.equal(result, null) +}) + +test('getOrganization - handles GraphQL errors', async () => { + const mockGraphql = async () => { + throw new Error('API rate limit exceeded') + } + + await assert.rejects( + async () => { + await getOrganization(mockGraphql, 'test-org') + }, + { + message: /Failed to fetch organization: API rate limit exceeded/ + }, + 'Should wrap GraphQL errors' + ) +}) + +// Integration test with real API +test( + 'getOrganization - real API call', + { + skip: !process.env.GH_PAT && !process.env.GH_PRIVATE_KEY + }, + async () => { + const { getOrganization: getOrgAPI } = await import('../src/index.js') + + // Fetch gitevents organization + const org = await getOrgAPI('gitevents') + + assert.ok(org, 'Should return organization data') + assert.equal(org.login, 'gitevents') + assert.ok(org.name, 'Should have name') + assert.ok(typeof org.memberCount === 'number', 'Should have member count') + assert.ok( + typeof org.publicRepoCount === 'number', + 'Should have public repo count' + ) + assert.ok('description' in org, 'Should have description field') + assert.ok('websiteUrl' in org, 'Should have websiteUrl field') + assert.ok(org.avatarUrl, 'Should have avatar URL') + } +)