chittycan
Version:
Your completely autonomous network that grows with you - DNA ownership platform with encrypted vaults, PDX portability, and ChittyFoundation governance
228 lines • 9.72 kB
JavaScript
import { NotionClient } from "./notion.js";
import { GitHubClient } from "./github.js";
const DEFAULT_MAPPINGS = [
{ notionStatus: "To Do", githubState: "open", githubLabels: ["todo"] },
{ notionStatus: "In Progress", githubState: "open", githubLabels: ["in-progress"] },
{ notionStatus: "Done", githubState: "closed", githubLabels: ["done"] },
{ notionStatus: "Archived", githubState: "closed", githubLabels: ["archived"] }
];
export class SyncWorker {
notion;
github;
config;
mappings;
constructor(config, mappings) {
this.notion = new NotionClient(config.notionToken);
this.github = new GitHubClient(config.githubToken);
this.config = config;
this.mappings = mappings || DEFAULT_MAPPINGS;
}
async sync() {
const result = {
createdInNotion: 0,
createdInGitHub: 0,
updatedInNotion: 0,
updatedInGitHub: 0,
conflicts: [],
errors: []
};
try {
// Fetch all data
const [actions, issues] = await Promise.all([
this.notion.queryDatabase(this.config.notionDatabaseId),
this.github.listIssues(this.config.githubOwner, this.config.githubRepo)
]);
// Build lookup maps
const actionsById = new Map(actions.map(a => [a.id, a]));
const actionsByIssueNumber = new Map(actions.filter(a => a.githubIssueNumber).map(a => [a.githubIssueNumber, a]));
const issuesByNumber = new Map(issues.map(i => [i.number, i]));
// Process GitHub issues → Notion
for (const issue of issues) {
const action = actionsByIssueNumber.get(issue.number);
if (!action) {
// Create new action in Notion
if (!this.config.dryRun) {
try {
const newAction = this.issueToAction(issue);
await this.notion.createPage(this.config.notionDatabaseId, newAction);
result.createdInNotion++;
}
catch (error) {
result.errors.push({
item: `GitHub Issue #${issue.number}`,
error: error.message
});
}
}
else {
result.createdInNotion++;
}
}
else {
// Check for conflicts or updates
const conflict = this.detectConflict(action, issue);
if (conflict) {
result.conflicts.push(conflict);
}
else if (this.needsUpdate(action, issue, "from-github")) {
// Update Notion from GitHub
if (!this.config.dryRun) {
try {
await this.notion.updatePage(action.id, this.getUpdatesFromIssue(issue));
result.updatedInNotion++;
}
catch (error) {
result.errors.push({
item: `Notion Action ${action.title}`,
error: error.message
});
}
}
else {
result.updatedInNotion++;
}
}
}
}
// Process Notion actions → GitHub
for (const action of actions) {
if (!action.githubIssueNumber) {
// Create new issue in GitHub
if (!this.config.dryRun) {
try {
const issue = await this.createIssueFromAction(action);
await this.notion.updatePage(action.id, {
githubIssueNumber: issue.number,
githubIssueUrl: issue.html_url,
githubRepo: issue.repository,
syncState: "synced",
lastSync: new Date().toISOString()
});
result.createdInGitHub++;
}
catch (error) {
result.errors.push({
item: `Notion Action ${action.title}`,
error: error.message
});
}
}
else {
result.createdInGitHub++;
}
}
else {
const issue = issuesByNumber.get(action.githubIssueNumber);
if (issue && this.needsUpdate(action, issue, "from-notion")) {
// Update GitHub from Notion
if (!this.config.dryRun) {
try {
await this.updateIssueFromAction(action, issue);
await this.notion.updatePage(action.id, {
syncState: "synced",
lastSync: new Date().toISOString()
});
result.updatedInGitHub++;
}
catch (error) {
result.errors.push({
item: `GitHub Issue #${action.githubIssueNumber}`,
error: error.message
});
}
}
else {
result.updatedInGitHub++;
}
}
}
}
return result;
}
catch (error) {
result.errors.push({
item: "Sync process",
error: error.message
});
return result;
}
}
issueToAction(issue) {
const mapping = this.getMapping(issue.state, issue);
return {
id: "",
title: issue.title,
status: mapping.notionStatus,
due: issue.due_on,
assignee: issue.assignees?.[0],
notes: issue.body,
githubIssueUrl: issue.html_url,
githubIssueNumber: issue.number,
githubRepo: issue.repository,
syncState: "synced",
lastSync: new Date().toISOString()
};
}
async createIssueFromAction(action) {
const mapping = this.mappings.find(m => m.notionStatus === action.status);
return await this.github.createIssue(this.config.githubOwner, this.config.githubRepo, action.title, action.notes, mapping?.githubLabels);
}
async updateIssueFromAction(action, issue) {
const mapping = this.mappings.find(m => m.notionStatus === action.status);
await this.github.updateIssue(this.config.githubOwner, this.config.githubRepo, issue.number, {
title: action.title,
body: action.notes,
state: mapping?.githubState,
labels: mapping?.githubLabels
});
}
getUpdatesFromIssue(issue) {
const mapping = this.getMapping(issue.state, issue);
return {
title: issue.title,
status: mapping.notionStatus,
notes: issue.body,
syncState: "synced",
lastSync: new Date().toISOString()
};
}
getMapping(githubState, issue) {
// Try to find specific mapping (future: could check labels)
const defaultMapping = this.mappings.find(m => m.githubState === githubState);
return defaultMapping || this.mappings[0];
}
needsUpdate(action, issue, direction) {
if (!action.lastSync)
return true;
const lastSync = new Date(action.lastSync).getTime();
const now = Date.now();
// Simple heuristic: if changed in the last hour, check for differences
if (now - lastSync < 3600000)
return false;
if (direction === "from-github") {
// Check if GitHub has newer data
return action.title !== issue.title || action.notes !== issue.body;
}
else {
// Check if Notion has newer data
const mapping = this.mappings.find(m => m.notionStatus === action.status);
return issue.state !== mapping?.githubState || issue.title !== action.title;
}
}
detectConflict(action, issue) {
if (action.syncState === "conflict") {
return { action, issue, reason: "Previous conflict not resolved" };
}
// Both changed since last sync
if (action.lastSync) {
const lastSync = new Date(action.lastSync).getTime();
const notionChanged = action.title !== issue.title || action.notes !== issue.body;
const githubChanged = issue.title !== action.title || issue.body !== action.notes;
if (notionChanged && githubChanged) {
return { action, issue, reason: "Both Notion and GitHub modified since last sync" };
}
}
return null;
}
}
//# sourceMappingURL=sync.js.map