Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 and validate location data with consistent schema
- πŸ‘€ Fetch user profiles and speaker information
- πŸ“„ Fetch file contents from repositories (text files, JSON, etc.)
- πŸ‘₯ Fetch GitHub Teams and team members
Expand Down Expand Up @@ -303,6 +304,86 @@ const config = await getFile('myorg', 'myrepo', 'config.json', {
- Throws `Binary files are not supported` error for binary files
- Throws `Failed to parse JSON` error if parse: true but content is invalid JSON

### `getLocations(org, repo, options?)`

Fetch and validate location data from a repository with consistent schema.

**Parameters:**

- `org` (string) - GitHub organization or user name
- `repo` (string) - Repository name
- `options` (object, optional) - Options
- `fileName` (string) - File name (default: 'locations.json')
- `branch` (string) - Branch name (default: 'HEAD')

**Returns:** `Promise<{ locations: Location[], errors: Error[] | null }>`

Returns validated locations and any validation errors.

**Example:**

```javascript
import { getLocations } from 'gitevents-fetch'

const result = await getLocations('myorg', 'events')

console.log(result.locations)
// [
// {
// id: 'venue-1',
// name: 'Tech Hub',
// address: '123 Main St, City',
// coordinates: { lat: 40.7128, lng: -74.006 },
// url: 'https://techhub.com',
// what3words: 'filled.count.soap',
// description: 'A modern tech venue',
// capacity: 100,
// accessibility: 'Wheelchair accessible'
// }
// ]

// Check for validation errors
if (result.errors) {
console.log('Invalid locations:', result.errors)
}

// Use custom file name
const venues = await getLocations('myorg', 'events', {
fileName: 'venues.json'
})
```

**Location Schema:**

Required fields:

- `id` (string) - Unique identifier
- `name` (string) - Location name

Optional fields (null if not provided):

- `address` (string) - Physical address
- `coordinates` (object) - { lat: number, lng: number }
- `url` (string) - Location website
- `what3words` (string) - what3words address
- `description` (string) - Location description
- `capacity` (number) - Venue capacity
- `accessibility` (string) - Accessibility information
- Custom fields are preserved

**Validation:**

The function validates each location and returns:

- `locations` - Array of valid, normalized locations
- `errors` - Array of validation errors (null if all valid)

Each error includes:

- `index` - Array index of invalid location
- `id` - Location ID (if available)
- `errors` - Array of validation error messages

### Fetching Talks from a Dedicated Repository

Talks stored as issues in a dedicated repository can be fetched using the existing event functions:
Expand Down
9 changes: 9 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 { getLocations as fetchLocations } from './locations.js'
import { getUser as getUserProfile } from './users.js'
import { getFile as getFileContent } from './files.js'

Expand Down Expand Up @@ -101,3 +102,11 @@ export async function getUser(login) {
}
return getUserProfile(getGraphqlClient(), login)
}

export async function getLocations(org, repo, options) {
// Validate parameters before creating auth
if (!org || !repo) {
throw new Error('Missing required parameters: org and repo are required')
}
return fetchLocations(getGraphqlClient(), org, repo, options)
}
137 changes: 137 additions & 0 deletions src/locations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { getFile } from './files.js'

Copilot AI Oct 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imported module './files.js' does not exist in the codebase. This will cause a runtime error when the getLocations function is called. Either create the missing files.js module or replace this with the appropriate file-fetching implementation using the graphql client directly.

Copilot uses AI. Check for mistakes.

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(', ')}`)
}
}

Comment on lines +2 to +12

Copilot AI Oct 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated code: The validateParams function is duplicated from src/lib/validateParams.js. Import and use the existing utility function instead of redefining it.

Suggested change
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(', ')}`)
}
}
import { validateParams } from './lib/validateParams.js'

Copilot uses AI. Check for mistakes.
function validateLocationSchema(location) {
// Basic schema validation for location objects
const errors = []

if (!location.id || typeof location.id !== 'string') {
errors.push('Location must have a string id')
}

if (!location.name || typeof location.name !== 'string') {
errors.push('Location must have a string name')
}

// Optional fields validation
if (location.address && typeof location.address !== 'string') {
errors.push('Location address must be a string')
}

if (location.coordinates) {
if (typeof location.coordinates !== 'object') {
errors.push('Location coordinates must be an object')
} else {
if (
!('lat' in location.coordinates) ||
typeof location.coordinates.lat !== 'number'
) {
errors.push('Location coordinates.lat must be a number')
}
if (
!('lng' in location.coordinates) ||
typeof location.coordinates.lng !== 'number'
) {
errors.push('Location coordinates.lng must be a number')
}
}
}

return errors
}

export async function getLocations(graphql, org, repo, options = {}) {
validateParams({ graphql, org, repo })

try {
const fileName = options.fileName || 'locations.json'
const branch = options.branch || 'HEAD'

// Fetch and parse the locations file
const locationsData = await getFile(graphql, org, repo, fileName, {
parse: true,
branch
})

// Ensure it's an array
if (!Array.isArray(locationsData)) {
throw new Error('Locations file must contain an array of locations')
}

// Validate each location
const validatedLocations = []
const validationErrors = []

for (let i = 0; i < locationsData.length; i++) {
const location = locationsData[i]
const errors = validateLocationSchema(location)

if (errors.length > 0) {
validationErrors.push({
index: i,
id: location.id || 'unknown',
errors
})
} else {
// Normalize the location object with consistent schema
validatedLocations.push({
id: location.id,
name: location.name,
address: location.address || null,
coordinates: location.coordinates
? {
lat: location.coordinates.lat,
lng: location.coordinates.lng
}
: null,
url: location.url || null,
what3words: location.what3words || null,
description: location.description || null,
capacity: location.capacity || null,

Copilot AI Oct 29, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect falsy value handling: Using || will convert capacity: 0 to null. Use nullish coalescing (??) instead to preserve zero values.

Suggested change
capacity: location.capacity || null,
capacity: location.capacity ?? null,

Copilot uses AI. Check for mistakes.
accessibility: location.accessibility || null,
// Include any additional custom fields
...Object.fromEntries(
Object.entries(location).filter(
([key]) =>
![
'id',
'name',
'address',
'coordinates',
'url',
'what3words',
'description',
'capacity',
'accessibility'
].includes(key)
)
)
})
}
}

// If validation errors, include them in the response
return {
locations: validatedLocations,
errors: validationErrors.length > 0 ? validationErrors : null
}
} catch (error) {
// Check if it's a file-related error
if (
error.message.includes('File not found') ||
error.message.includes('Failed to parse JSON')
) {
throw error
}
throw new Error(`Failed to fetch locations: ${error.message}`)
}
}
Loading
Loading