Skip to main content

Git Operations Services

The Ever Works Template uses a Git-based content management system where YAML files stored in a GitHub repository serve as the source of truth. Four dedicated Git service classes handle CRUD operations and synchronization for items, categories, tags, and collections.

Architecture Overview

All Git services share a common architecture built on top of the isomorphic-git library, enabling server-side Git operations without a native Git binary.

ServiceFileData FormatStorage
ItemGitServiceitem-git.service.tsPer-item YAML directoriesdata/{slug}/{slug}.yml
CategoryGitServicecategory-git.service.tsSingle YAML filecategories.yml
TagGitServicetag-git.service.tsSingle YAML filetags.yml
CollectionGitServicecollection-git.service.tsSingle YAML filecollections.yml
.content/
.git/
data/
my-item/
my-item.yml
categories.yml
tags.yml
collections.yml

ItemGitService

The ItemGitService manages individual content items. Each item is stored as a YAML file inside its own directory, identified by its slug.

Configuration

interface ItemGitServiceConfig {
owner: string; // GitHub repository owner
repo: string; // Repository name
token: string; // GitHub personal access token
branch: string; // Target branch (e.g., "main")
dataDir: string; // Local directory path (e.g., ".content")
itemsDir: string; // Items subdirectory (e.g., "data")
}

Initialization

The service initializes by ensuring directory structure exists and syncing with the remote repository:

const service = await createItemGitService({
owner: 'ever-works',
repo: 'my-data',
token: process.env.GITHUB_TOKEN,
branch: 'main',
dataDir: '.content',
itemsDir: 'data',
});

During initialization, the service either clones the repository (first run) or pulls the latest changes from the remote.

Item CRUD Operations

MethodDescriptionGit Commit
createItem(data)Creates a new item with duplicate detectionYes
updateItem(id, data)Updates an existing item by IDYes
updateItemWithoutCommit(id, data)Updates locally without Git pushNo
deleteItem(id)Permanently removes an item fileYes
softDeleteItem(id)Sets deleted_at timestampYes
restoreItem(id)Clears deleted_at to restore itemYes
reviewItem(id, reviewData)Updates status with review metadataYes

Reading Items

// Read all items (excludes soft-deleted by default)
const items = await service.readItems();

// Include soft-deleted items
const allItems = await service.readItems(true);

// Read specific items by slug (efficient targeted read)
const specific = await service.readItemsBySlugs(['item-one', 'item-two']);

// Find single item
const item = await service.findItemById('my-item-id');
const bySlug = await service.findItemBySlug('my-item-slug');

Paginated Queries

The service supports server-side pagination with filtering and sorting:

const result = await service.getItemsPaginated(1, 10, {
status: 'approved',
categories: ['tools', 'apps'],
tags: ['open-source'],
search: 'project management',
sortBy: 'updated_at',
sortOrder: 'desc',
includeDeleted: false,
submittedBy: 'user-123',
});
// Returns: { items, total, page, limit, totalPages }

Location Indexing Integration

When items contain location data, the ItemGitService automatically triggers asynchronous location indexing. This runs in the background and does not block the main operation:

// On create/update with location data -> indexes location
// On delete/soft-delete -> removes from location index

Batch Operations

For bulk updates, use updateItemWithoutCommit followed by a single batch commit:

for (const item of itemsToUpdate) {
await service.updateItemWithoutCommit(item.id, { featured: true });
}
await service.commitAndPushBatch('Batch: Mark items as featured');

CategoryGitService

Categories are stored in a single categories.yml file. The service manages the full lifecycle with Git synchronization.

Key Methods

const categoryService = await createCategoryGitService(gitConfig, '.content');

// CRUD operations
const category = await categoryService.createCategory({ id: 'tools', name: 'Tools' });
const updated = await categoryService.updateCategory({ id: 'tools', name: 'Dev Tools' });
await categoryService.deleteCategory('tools');

// Read operations
const categories = await categoryService.readCategories();

// Repository status
const status = await categoryService.getStatus();
// { repoUrl, branch, lastSync, categoriesCount }

Duplicate Detection

Both createCategory and updateCategory check for duplicate IDs and names (case-insensitive) before writing:

// Throws: 'Category with ID "tools" already exists'
// Throws: 'Category with name "Tools" already exists'

TagGitService

Tags are stored in tags.yml with support for an isActive flag for enabling/disabling tags without deletion.

Tag Data Structure

interface TagData {
id: string;
name: string;
isActive: boolean; // Defaults to true for backward compatibility
}

Key Methods

const tagService = await createTagGitService(config);

// CRUD
const tag = await tagService.createTag({ id: 'react', name: 'React', isActive: true });
const updated = await tagService.updateTag('react', { name: 'React.js' });
await tagService.deleteTag('react');

// Querying
const allTags = await tagService.getAllTags();
const byName = await tagService.findTagByName('React');
const paginated = await tagService.getTagsPaginated(1, 20);

// Duplicate checking
const isDuplicateName = await tagService.checkDuplicateName('react', excludeId);
const isDuplicateId = await tagService.checkDuplicateId('react');

CollectionGitService

Collections group items together and are stored in collections.yml. They support slugs, descriptions, icons, and item counts.

Collection Data Structure

interface Collection {
id: string;
slug: string;
name: string;
description: string;
icon_url?: string;
isActive: boolean;
item_count: number;
created_at: string;
updated_at: string;
}

Pending Changes Merging

The CollectionGitService includes a sophisticated merge strategy for pending changes. When multiple writes occur before a Git push succeeds, the service merges them by collection ID, keeping the most recent version:

// Internal merge logic preserves latest edits
// and prevents older pending state from overwriting newer changes
private mergePendingChanges(next: Collection[]): void {
// Uses Map to deduplicate by ID, preferring `next` (newer)
}

Background Sync and Resilience

All four Git services implement a resilient synchronization pattern with automatic retry.

Sync Flow

  1. Local write first -- The YAML file is always written locally before attempting Git operations
  2. Commit and push -- Changes are staged, committed, and pushed to GitHub
  3. Failure handling -- If Git operations fail, changes are stored as pending
  4. Background retry -- A background process retries with exponential backoff

Retry Configuration

ParameterValue
Initial retry delay30 seconds
Backoff multiplier2x
Maximum delay5 minutes
Maximum retries3

Sync Status

Each service exposes its synchronization state:

const status = await service.getSyncStatus();
// {
// hasPendingChanges: boolean,
// syncInProgress: boolean,
// lastSyncAttempt?: string,
// retryCount?: number
// }

Cleanup

The CategoryGitService, TagGitService, and CollectionGitService provide a cleanup() method to stop retry timers and release resources:

service.cleanup(); // Clears timeouts, resets state

Authentication

All services authenticate with GitHub using the personal access token pattern:

// Authentication via x-access-token
{ username: 'x-access-token', password: config.token }

Commits are attributed to a configured committer identity:

// Default committer
{ name: 'Ever Works Admin', email: 'admin@everworks.com' }
// Or environment-based
{ name: process.env.GIT_NAME, email: process.env.GIT_EMAIL }

Source Files

FilePath
Item Git Servicetemplate/lib/services/item-git.service.ts
Category Git Servicetemplate/lib/services/category-git.service.ts
Tag Git Servicetemplate/lib/services/tag-git.service.ts
Collection Git Servicetemplate/lib/services/collection-git.service.ts