ynab-mcp-deluxe 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +25 -0
- package/README.md +99 -0
- package/dist/backup.d.ts +28 -0
- package/dist/backup.d.ts.map +1 -0
- package/dist/backup.js +78 -0
- package/dist/drift-detection.d.ts +110 -0
- package/dist/drift-detection.d.ts.map +1 -0
- package/dist/drift-detection.js +366 -0
- package/dist/drift-snapshot.d.ts +74 -0
- package/dist/drift-snapshot.d.ts.map +1 -0
- package/dist/drift-snapshot.js +120 -0
- package/dist/helpers.d.ts +65 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +226 -0
- package/dist/local-budget.d.ts +86 -0
- package/dist/local-budget.d.ts.map +1 -0
- package/dist/local-budget.js +277 -0
- package/dist/logger.d.ts +50 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +136 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1626 -0
- package/dist/sync-history.d.ts +95 -0
- package/dist/sync-history.d.ts.map +1 -0
- package/dist/sync-history.js +205 -0
- package/dist/sync-providers.d.ts +71 -0
- package/dist/sync-providers.d.ts.map +1 -0
- package/dist/sync-providers.js +105 -0
- package/dist/types.d.ts +535 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/ynab-client.d.ts +257 -0
- package/dist/ynab-client.d.ts.map +1 -0
- package/dist/ynab-client.js +1248 -0
- package/package.json +102 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync history persistence utilities.
|
|
3
|
+
*
|
|
4
|
+
* Every sync response is saved to disk, creating an automatic incremental backup trail.
|
|
5
|
+
* This replaces the auto-backup functionality with continuous, granular backups.
|
|
6
|
+
*
|
|
7
|
+
* Directory structure:
|
|
8
|
+
* ~/.config/ynab-mcp-deluxe/sync-history/[budgetId]/
|
|
9
|
+
* - 20260125T143022Z-full.json # Initial sync
|
|
10
|
+
* - 20260125T153022Z-delta.json # Delta sync
|
|
11
|
+
* - ...
|
|
12
|
+
*/
|
|
13
|
+
import type { SyncType } from './types.js';
|
|
14
|
+
import type { BudgetDetail } from 'ynab';
|
|
15
|
+
/**
|
|
16
|
+
* Logger interface matching FastMCP's context log
|
|
17
|
+
*/
|
|
18
|
+
interface ContextLog {
|
|
19
|
+
debug: (message: string, data?: unknown) => void;
|
|
20
|
+
error: (message: string, data?: unknown) => void;
|
|
21
|
+
info: (message: string, data?: unknown) => void;
|
|
22
|
+
warn: (message: string, data?: unknown) => void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get the base sync history directory path.
|
|
26
|
+
* ~/.config/ynab-mcp-deluxe/sync-history/
|
|
27
|
+
*/
|
|
28
|
+
export declare function getSyncHistoryBaseDir(): string;
|
|
29
|
+
/**
|
|
30
|
+
* Validate that a budgetId is safe to use in file paths.
|
|
31
|
+
* YNAB budget IDs are UUIDs (alphanumeric with hyphens).
|
|
32
|
+
* This prevents path traversal attacks with malicious budgetId values.
|
|
33
|
+
*
|
|
34
|
+
* @param budgetId - The budget ID to validate
|
|
35
|
+
* @returns true if the budgetId is safe for use in paths
|
|
36
|
+
*/
|
|
37
|
+
export declare function isValidBudgetIdForPath(budgetId: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Get the sync history directory for a specific budget.
|
|
40
|
+
* ~/.config/ynab-mcp-deluxe/sync-history/[budgetId]/
|
|
41
|
+
*
|
|
42
|
+
* @param budgetId - The budget ID (must be a valid UUID format)
|
|
43
|
+
* @throws Error if budgetId contains invalid characters (path traversal protection)
|
|
44
|
+
*/
|
|
45
|
+
export declare function getSyncHistoryDir(budgetId: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Ensure the sync history directory exists for a budget.
|
|
48
|
+
* Creates the directory tree if it doesn't exist.
|
|
49
|
+
*/
|
|
50
|
+
export declare function ensureSyncHistoryDir(budgetId: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Generate sync history filename with timestamp and sync type.
|
|
53
|
+
* Format: YYYYMMDDTHHMMSSZ-[full|delta].json
|
|
54
|
+
*
|
|
55
|
+
* Uses compact ISO 8601 format for easy sorting and readability.
|
|
56
|
+
* Example: 20260125T143022Z-full.json
|
|
57
|
+
*/
|
|
58
|
+
export declare function generateSyncFilename(syncType: SyncType): string;
|
|
59
|
+
/**
|
|
60
|
+
* Persist a sync response to disk.
|
|
61
|
+
*
|
|
62
|
+
* This function is designed to be fail-safe: if the write fails,
|
|
63
|
+
* it logs a warning but does not throw. The sync operation should
|
|
64
|
+
* continue even if persistence fails.
|
|
65
|
+
*
|
|
66
|
+
* @param budgetId - The budget ID
|
|
67
|
+
* @param syncType - 'full' or 'delta'
|
|
68
|
+
* @param budget - The budget data from YNAB API
|
|
69
|
+
* @param serverKnowledge - The server_knowledge from this sync
|
|
70
|
+
* @param previousServerKnowledge - For delta syncs, the previous server_knowledge
|
|
71
|
+
* @param log - Logger for debug/error output
|
|
72
|
+
* @returns The file path if successful, null if failed
|
|
73
|
+
*/
|
|
74
|
+
export declare function persistSyncResponse(budgetId: string, syncType: SyncType, budget: BudgetDetail, serverKnowledge: number, previousServerKnowledge: number | null, log: ContextLog): Promise<string | null>;
|
|
75
|
+
/**
|
|
76
|
+
* Result of clearing sync history
|
|
77
|
+
*/
|
|
78
|
+
export interface ClearSyncHistoryResult {
|
|
79
|
+
/** Budget IDs that were cleared (or 'all' if no specific budget) */
|
|
80
|
+
budgetsCleared: string[];
|
|
81
|
+
/** Any errors encountered (non-fatal) */
|
|
82
|
+
errors: string[];
|
|
83
|
+
/** Number of files deleted */
|
|
84
|
+
filesDeleted: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Clear sync history for a specific budget or all budgets.
|
|
88
|
+
*
|
|
89
|
+
* @param budgetId - Optional budget ID to clear. If not provided, clears all budgets.
|
|
90
|
+
* @param log - Logger for debug/error output
|
|
91
|
+
* @returns Summary of the clear operation
|
|
92
|
+
*/
|
|
93
|
+
export declare function clearSyncHistory(budgetId: string | null, log: ContextLog): Promise<ClearSyncHistoryResult>;
|
|
94
|
+
export {};
|
|
95
|
+
//# sourceMappingURL=sync-history.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-history.d.ts","sourceRoot":"","sources":["../src/sync-history.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAmB,QAAQ,EAAC,MAAM,YAAY,CAAC;AAC3D,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,MAAM,CAAC;AAMvC;;GAEG;AACH,UAAU,UAAU;IAClB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACjD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CACjD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAKhE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO1D;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1E;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAS/D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,YAAY,EACpB,eAAe,EAAE,MAAM,EACvB,uBAAuB,EAAE,MAAM,GAAG,IAAI,EACtC,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA4CxB;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,oEAAoE;IACpE,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,yCAAyC;IACzC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,8BAA8B;IAC9B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,GAAG,EAAE,UAAU,GACd,OAAO,CAAC,sBAAsB,CAAC,CAmEjC"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync history persistence utilities.
|
|
3
|
+
*
|
|
4
|
+
* Every sync response is saved to disk, creating an automatic incremental backup trail.
|
|
5
|
+
* This replaces the auto-backup functionality with continuous, granular backups.
|
|
6
|
+
*
|
|
7
|
+
* Directory structure:
|
|
8
|
+
* ~/.config/ynab-mcp-deluxe/sync-history/[budgetId]/
|
|
9
|
+
* - 20260125T143022Z-full.json # Initial sync
|
|
10
|
+
* - 20260125T153022Z-delta.json # Delta sync
|
|
11
|
+
* - ...
|
|
12
|
+
*/
|
|
13
|
+
import { mkdir, readdir, rm, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
/**
|
|
17
|
+
* Get the base sync history directory path.
|
|
18
|
+
* ~/.config/ynab-mcp-deluxe/sync-history/
|
|
19
|
+
*/
|
|
20
|
+
export function getSyncHistoryBaseDir() {
|
|
21
|
+
return join(homedir(), '.config', 'ynab-mcp-deluxe', 'sync-history');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validate that a budgetId is safe to use in file paths.
|
|
25
|
+
* YNAB budget IDs are UUIDs (alphanumeric with hyphens).
|
|
26
|
+
* This prevents path traversal attacks with malicious budgetId values.
|
|
27
|
+
*
|
|
28
|
+
* @param budgetId - The budget ID to validate
|
|
29
|
+
* @returns true if the budgetId is safe for use in paths
|
|
30
|
+
*/
|
|
31
|
+
export function isValidBudgetIdForPath(budgetId) {
|
|
32
|
+
// YNAB budget IDs are UUIDs: alphanumeric characters and hyphens only
|
|
33
|
+
// Example: "12345678-1234-1234-1234-123456789abc"
|
|
34
|
+
// Reject empty strings, path separators, dots, and other special characters
|
|
35
|
+
return /^[a-zA-Z0-9-]+$/.test(budgetId) && budgetId.length > 0;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the sync history directory for a specific budget.
|
|
39
|
+
* ~/.config/ynab-mcp-deluxe/sync-history/[budgetId]/
|
|
40
|
+
*
|
|
41
|
+
* @param budgetId - The budget ID (must be a valid UUID format)
|
|
42
|
+
* @throws Error if budgetId contains invalid characters (path traversal protection)
|
|
43
|
+
*/
|
|
44
|
+
export function getSyncHistoryDir(budgetId) {
|
|
45
|
+
if (!isValidBudgetIdForPath(budgetId)) {
|
|
46
|
+
throw new Error(`Invalid budgetId for file path: "${budgetId}". Budget IDs must contain only alphanumeric characters and hyphens.`);
|
|
47
|
+
}
|
|
48
|
+
return join(getSyncHistoryBaseDir(), budgetId);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Ensure the sync history directory exists for a budget.
|
|
52
|
+
* Creates the directory tree if it doesn't exist.
|
|
53
|
+
*/
|
|
54
|
+
export async function ensureSyncHistoryDir(budgetId) {
|
|
55
|
+
const dir = getSyncHistoryDir(budgetId);
|
|
56
|
+
await mkdir(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Generate sync history filename with timestamp and sync type.
|
|
60
|
+
* Format: YYYYMMDDTHHMMSSZ-[full|delta].json
|
|
61
|
+
*
|
|
62
|
+
* Uses compact ISO 8601 format for easy sorting and readability.
|
|
63
|
+
* Example: 20260125T143022Z-full.json
|
|
64
|
+
*/
|
|
65
|
+
export function generateSyncFilename(syncType) {
|
|
66
|
+
const now = new Date();
|
|
67
|
+
// Format: 20260125T143022Z (compact ISO 8601 UTC)
|
|
68
|
+
const timestamp = now
|
|
69
|
+
.toISOString()
|
|
70
|
+
.replace(/[-:]/g, '')
|
|
71
|
+
.replace(/\.\d+Z$/, 'Z');
|
|
72
|
+
return `${timestamp}-${syncType}.json`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Persist a sync response to disk.
|
|
76
|
+
*
|
|
77
|
+
* This function is designed to be fail-safe: if the write fails,
|
|
78
|
+
* it logs a warning but does not throw. The sync operation should
|
|
79
|
+
* continue even if persistence fails.
|
|
80
|
+
*
|
|
81
|
+
* @param budgetId - The budget ID
|
|
82
|
+
* @param syncType - 'full' or 'delta'
|
|
83
|
+
* @param budget - The budget data from YNAB API
|
|
84
|
+
* @param serverKnowledge - The server_knowledge from this sync
|
|
85
|
+
* @param previousServerKnowledge - For delta syncs, the previous server_knowledge
|
|
86
|
+
* @param log - Logger for debug/error output
|
|
87
|
+
* @returns The file path if successful, null if failed
|
|
88
|
+
*/
|
|
89
|
+
export async function persistSyncResponse(budgetId, syncType, budget, serverKnowledge, previousServerKnowledge, log) {
|
|
90
|
+
const startTime = performance.now();
|
|
91
|
+
try {
|
|
92
|
+
// Ensure directory exists
|
|
93
|
+
await ensureSyncHistoryDir(budgetId);
|
|
94
|
+
// Build the sync history entry
|
|
95
|
+
const entry = {
|
|
96
|
+
budget,
|
|
97
|
+
previousServerKnowledge,
|
|
98
|
+
serverKnowledge,
|
|
99
|
+
syncType,
|
|
100
|
+
syncedAt: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
// Generate filename and path
|
|
103
|
+
const filename = generateSyncFilename(syncType);
|
|
104
|
+
const filePath = join(getSyncHistoryDir(budgetId), filename);
|
|
105
|
+
// Write to disk
|
|
106
|
+
await writeFile(filePath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
107
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
108
|
+
log.debug('Sync history persisted', {
|
|
109
|
+
budgetId,
|
|
110
|
+
durationMs,
|
|
111
|
+
filePath,
|
|
112
|
+
serverKnowledge,
|
|
113
|
+
syncType,
|
|
114
|
+
});
|
|
115
|
+
return filePath;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
// Log warning but don't fail - persistence is a safety feature, not critical path
|
|
119
|
+
const durationMs = Math.round(performance.now() - startTime);
|
|
120
|
+
log.warn('Failed to persist sync history (continuing with sync)', {
|
|
121
|
+
budgetId,
|
|
122
|
+
durationMs,
|
|
123
|
+
error: error instanceof Error ? error.message : String(error),
|
|
124
|
+
syncType,
|
|
125
|
+
});
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Clear sync history for a specific budget or all budgets.
|
|
131
|
+
*
|
|
132
|
+
* @param budgetId - Optional budget ID to clear. If not provided, clears all budgets.
|
|
133
|
+
* @param log - Logger for debug/error output
|
|
134
|
+
* @returns Summary of the clear operation
|
|
135
|
+
*/
|
|
136
|
+
export async function clearSyncHistory(budgetId, log) {
|
|
137
|
+
const result = {
|
|
138
|
+
budgetsCleared: [],
|
|
139
|
+
errors: [],
|
|
140
|
+
filesDeleted: 0,
|
|
141
|
+
};
|
|
142
|
+
const baseDir = getSyncHistoryBaseDir();
|
|
143
|
+
try {
|
|
144
|
+
if (budgetId !== null) {
|
|
145
|
+
// Clear specific budget
|
|
146
|
+
const budgetDir = getSyncHistoryDir(budgetId);
|
|
147
|
+
try {
|
|
148
|
+
const files = await readdir(budgetDir);
|
|
149
|
+
result.filesDeleted = files.length;
|
|
150
|
+
await rm(budgetDir, { force: true, recursive: true });
|
|
151
|
+
result.budgetsCleared.push(budgetId);
|
|
152
|
+
log.info('Cleared sync history for budget', {
|
|
153
|
+
budgetId,
|
|
154
|
+
filesDeleted: result.filesDeleted,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (error.code === 'ENOENT') {
|
|
159
|
+
// Directory doesn't exist - nothing to clear
|
|
160
|
+
log.info('No sync history to clear for budget', { budgetId });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Clear all budgets
|
|
169
|
+
try {
|
|
170
|
+
const budgetDirs = await readdir(baseDir);
|
|
171
|
+
for (const dir of budgetDirs) {
|
|
172
|
+
const budgetDir = getSyncHistoryDir(dir);
|
|
173
|
+
try {
|
|
174
|
+
const files = await readdir(budgetDir);
|
|
175
|
+
result.filesDeleted += files.length;
|
|
176
|
+
await rm(budgetDir, { force: true, recursive: true });
|
|
177
|
+
result.budgetsCleared.push(dir);
|
|
178
|
+
}
|
|
179
|
+
catch (dirError) {
|
|
180
|
+
result.errors.push(`Failed to clear ${dir}: ${dirError instanceof Error ? dirError.message : String(dirError)}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
log.info('Cleared all sync history', {
|
|
184
|
+
budgetsCleared: result.budgetsCleared.length,
|
|
185
|
+
filesDeleted: result.filesDeleted,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (error.code === 'ENOENT') {
|
|
190
|
+
// Base directory doesn't exist - nothing to clear
|
|
191
|
+
log.info('No sync history directory exists');
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
201
|
+
result.errors.push(errorMessage);
|
|
202
|
+
log.error('Failed to clear sync history', { budgetId, error: errorMessage });
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync providers for fetching budget data.
|
|
3
|
+
*
|
|
4
|
+
* SyncProvider is an abstraction that allows us to swap between:
|
|
5
|
+
* - ApiSyncProvider: Uses the YNAB API (production)
|
|
6
|
+
* - StaticSyncProvider: Loads from a JSON file (testing)
|
|
7
|
+
*
|
|
8
|
+
* This enables fast iteration and deterministic E2E tests.
|
|
9
|
+
*/
|
|
10
|
+
import type { SyncProvider } from './types.js';
|
|
11
|
+
import type { BudgetDetail } from 'ynab';
|
|
12
|
+
/**
|
|
13
|
+
* API-based sync provider that fetches from the YNAB API.
|
|
14
|
+
* This is the default provider for production use.
|
|
15
|
+
*/
|
|
16
|
+
export declare class ApiSyncProvider implements SyncProvider {
|
|
17
|
+
private api;
|
|
18
|
+
constructor(accessToken: string);
|
|
19
|
+
/**
|
|
20
|
+
* Perform a full sync - fetches the complete budget.
|
|
21
|
+
*/
|
|
22
|
+
fullSync(budgetId: string): Promise<{
|
|
23
|
+
budget: BudgetDetail;
|
|
24
|
+
serverKnowledge: number;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Perform a delta sync - fetches only changes since lastKnowledge.
|
|
28
|
+
*/
|
|
29
|
+
deltaSync(budgetId: string, lastKnowledge: number): Promise<{
|
|
30
|
+
budget: BudgetDetail;
|
|
31
|
+
serverKnowledge: number;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Static sync provider that loads from a JSON file.
|
|
36
|
+
* Used for testing to avoid hitting the YNAB API.
|
|
37
|
+
*
|
|
38
|
+
* Currently a stub - full implementation will be added later.
|
|
39
|
+
* See ROADMAP.md for "Static JSON Testing Mode" future enhancement.
|
|
40
|
+
*/
|
|
41
|
+
export declare class StaticSyncProvider implements SyncProvider {
|
|
42
|
+
private budgetData;
|
|
43
|
+
private serverKnowledge;
|
|
44
|
+
constructor(budgetData: BudgetDetail, serverKnowledge?: number);
|
|
45
|
+
/**
|
|
46
|
+
* Full sync returns the static budget data.
|
|
47
|
+
*/
|
|
48
|
+
fullSync(_budgetId: string): Promise<{
|
|
49
|
+
budget: BudgetDetail;
|
|
50
|
+
serverKnowledge: number;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Delta sync returns empty changes (no changes since static data is immutable).
|
|
54
|
+
* In the future, this could return changes from a mutations overlay.
|
|
55
|
+
*/
|
|
56
|
+
deltaSync(_budgetId: string, _lastKnowledge: number): Promise<{
|
|
57
|
+
budget: BudgetDetail;
|
|
58
|
+
serverKnowledge: number;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Create the appropriate sync provider based on environment.
|
|
63
|
+
*
|
|
64
|
+
* If YNAB_STATIC_BUDGET_FILE is set, loads from that file.
|
|
65
|
+
* Otherwise, uses the YNAB API with YNAB_ACCESS_TOKEN.
|
|
66
|
+
*
|
|
67
|
+
* @returns The configured SyncProvider
|
|
68
|
+
* @throws Error if required environment variables are missing
|
|
69
|
+
*/
|
|
70
|
+
export declare function createSyncProvider(): SyncProvider;
|
|
71
|
+
//# sourceMappingURL=sync-providers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-providers.d.ts","sourceRoot":"","sources":["../src/sync-providers.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,MAAM,CAAC;AAIvC;;;GAGG;AACH,qBAAa,eAAgB,YAAW,YAAY;IAClD,OAAO,CAAC,GAAG,CAAU;gBAET,WAAW,EAAE,MAAM;IAI/B;;OAEG;IACG,QAAQ,CACZ,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAC,MAAM,EAAE,YAAY,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAC,CAAC;IAQ3D;;OAEG;IACG,SAAS,CACb,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC;QAAC,MAAM,EAAE,YAAY,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAC,CAAC;CAU5D;AAED;;;;;;GAMG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,UAAU,CAAe;IACjC,OAAO,CAAC,eAAe,CAAS;gBAEpB,UAAU,EAAE,YAAY,EAAE,eAAe,SAAI;IAKzD;;OAEG;IACH,QAAQ,CACN,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC;QAAC,MAAM,EAAE,YAAY,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAC,CAAC;IAO3D;;;OAGG;IACH,SAAS,CACP,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC;QAAC,MAAM,EAAE,YAAY,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAC,CAAC;CAc5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,IAAI,YAAY,CAqBjD"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync providers for fetching budget data.
|
|
3
|
+
*
|
|
4
|
+
* SyncProvider is an abstraction that allows us to swap between:
|
|
5
|
+
* - ApiSyncProvider: Uses the YNAB API (production)
|
|
6
|
+
* - StaticSyncProvider: Loads from a JSON file (testing)
|
|
7
|
+
*
|
|
8
|
+
* This enables fast iteration and deterministic E2E tests.
|
|
9
|
+
*/
|
|
10
|
+
import { api as YnabApi } from 'ynab';
|
|
11
|
+
/**
|
|
12
|
+
* API-based sync provider that fetches from the YNAB API.
|
|
13
|
+
* This is the default provider for production use.
|
|
14
|
+
*/
|
|
15
|
+
export class ApiSyncProvider {
|
|
16
|
+
api;
|
|
17
|
+
constructor(accessToken) {
|
|
18
|
+
this.api = new YnabApi(accessToken);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Perform a full sync - fetches the complete budget.
|
|
22
|
+
*/
|
|
23
|
+
async fullSync(budgetId) {
|
|
24
|
+
const response = await this.api.budgets.getBudgetById(budgetId);
|
|
25
|
+
return {
|
|
26
|
+
budget: response.data.budget,
|
|
27
|
+
serverKnowledge: response.data.server_knowledge,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Perform a delta sync - fetches only changes since lastKnowledge.
|
|
32
|
+
*/
|
|
33
|
+
async deltaSync(budgetId, lastKnowledge) {
|
|
34
|
+
const response = await this.api.budgets.getBudgetById(budgetId, lastKnowledge);
|
|
35
|
+
return {
|
|
36
|
+
budget: response.data.budget,
|
|
37
|
+
serverKnowledge: response.data.server_knowledge,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Static sync provider that loads from a JSON file.
|
|
43
|
+
* Used for testing to avoid hitting the YNAB API.
|
|
44
|
+
*
|
|
45
|
+
* Currently a stub - full implementation will be added later.
|
|
46
|
+
* See ROADMAP.md for "Static JSON Testing Mode" future enhancement.
|
|
47
|
+
*/
|
|
48
|
+
export class StaticSyncProvider {
|
|
49
|
+
budgetData;
|
|
50
|
+
serverKnowledge;
|
|
51
|
+
constructor(budgetData, serverKnowledge = 1) {
|
|
52
|
+
this.budgetData = budgetData;
|
|
53
|
+
this.serverKnowledge = serverKnowledge;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Full sync returns the static budget data.
|
|
57
|
+
*/
|
|
58
|
+
fullSync(_budgetId) {
|
|
59
|
+
return Promise.resolve({
|
|
60
|
+
budget: this.budgetData,
|
|
61
|
+
serverKnowledge: this.serverKnowledge,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Delta sync returns empty changes (no changes since static data is immutable).
|
|
66
|
+
* In the future, this could return changes from a mutations overlay.
|
|
67
|
+
*/
|
|
68
|
+
deltaSync(_budgetId, _lastKnowledge) {
|
|
69
|
+
// Return an "empty" delta - same server knowledge, no changes
|
|
70
|
+
// The budget object will have empty/undefined arrays, indicating no changes
|
|
71
|
+
const emptyDelta = {
|
|
72
|
+
id: this.budgetData.id,
|
|
73
|
+
name: this.budgetData.name,
|
|
74
|
+
// All other fields are optional and undefined = no changes
|
|
75
|
+
};
|
|
76
|
+
return Promise.resolve({
|
|
77
|
+
budget: emptyDelta,
|
|
78
|
+
serverKnowledge: this.serverKnowledge,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Create the appropriate sync provider based on environment.
|
|
84
|
+
*
|
|
85
|
+
* If YNAB_STATIC_BUDGET_FILE is set, loads from that file.
|
|
86
|
+
* Otherwise, uses the YNAB API with YNAB_ACCESS_TOKEN.
|
|
87
|
+
*
|
|
88
|
+
* @returns The configured SyncProvider
|
|
89
|
+
* @throws Error if required environment variables are missing
|
|
90
|
+
*/
|
|
91
|
+
export function createSyncProvider() {
|
|
92
|
+
const staticBudgetFile = process.env['YNAB_STATIC_BUDGET_FILE'];
|
|
93
|
+
if (staticBudgetFile !== undefined && staticBudgetFile !== '') {
|
|
94
|
+
// Static mode - load from JSON file
|
|
95
|
+
// Note: Full implementation will be added in future enhancement
|
|
96
|
+
throw new Error(`Static budget file support not yet implemented. ` +
|
|
97
|
+
`Set YNAB_STATIC_BUDGET_FILE is set to: ${staticBudgetFile}`);
|
|
98
|
+
}
|
|
99
|
+
// API mode - use YNAB API
|
|
100
|
+
const accessToken = process.env['YNAB_ACCESS_TOKEN'];
|
|
101
|
+
if (accessToken === undefined || accessToken === '') {
|
|
102
|
+
throw new Error('YNAB authentication failed. Check that YNAB_ACCESS_TOKEN environment variable is set.');
|
|
103
|
+
}
|
|
104
|
+
return new ApiSyncProvider(accessToken);
|
|
105
|
+
}
|