diff --git a/README.md b/README.md index 83aaeed..5edd1e7 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,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 and validate location data with consistent schema - 👤 Fetch user profiles and speaker information - 📄 Fetch file contents from repositories (text files, JSON, etc.) @@ -220,6 +221,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 +// } +``` + ### `getUser(login)` Fetch a GitHub user profile (useful for speaker information). 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 b524628..f3aede2 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' import { getLocations as fetchLocations } from './locations.js' import { getUser as getUserProfile } from './users.js' import { getFile as getFileContent } from './files.js' @@ -110,3 +111,11 @@ export async function getLocations(org, repo, options) { } return fetchLocations(getGraphqlClient(), org, repo, options) } + +export async function getOrganization(org) { + // Validate parameters before creating auth + if (!org) { + throw new Error('Missing required parameters: org is required') + } + return getOrg(getGraphqlClient(), org) +} diff --git a/src/lib/parseGql.js b/src/lib/parseGql.js index 13c1aad..b024f7e 100644 --- a/src/lib/parseGql.js +++ b/src/lib/parseGql.js @@ -2,6 +2,7 @@ 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' import userQuery from '../graphql/user.gql?raw' import fileQuery from '../graphql/file.gql?raw' @@ -9,6 +10,7 @@ const queries = { events: eventsQuery, event: eventQuery, team: teamQuery, + organization: organizationQuery, user: userQuery, file: fileQuery } diff --git a/src/organization.js b/src/organization.js new file mode 100644 index 0000000..7ab6944 --- /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 orgData = result.organization + + return { + name: orgData.name || null, + login: orgData.login || null, + description: orgData.description || null, + websiteUrl: orgData.websiteUrl || null, + avatarUrl: orgData.avatarUrl || null, + email: orgData.email || null, + location: orgData.location || null, + createdAt: orgData.createdAt ? new Date(orgData.createdAt) : null, + updatedAt: orgData.updatedAt ? new Date(orgData.updatedAt) : null, + memberCount: orgData.membersWithRole?.totalCount || 0, + publicRepoCount: orgData.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..0224348 --- /dev/null +++ b/test/organization.test.js @@ -0,0 +1,156 @@ +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') + + try { + // 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') + } catch (error) { + // GitHub App may not have permission to access organization data + if (error.message.includes('Resource not accessible by integration')) { + console.log( + 'Note: GitHub App does not have permission to access organization data. This is expected in CI/CD environments.' + ) + // Skip this assertion if permissions are insufficient + assert.ok(true, 'Skipped due to insufficient permissions') + } else { + throw error + } + } + } +)