@hakimelek/monarchmoney 0.2.0 → 0.3.0
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/README.md +128 -0
- package/dist/cjs/client.js +923 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/endpoints.js +11 -0
- package/dist/cjs/endpoints.js.map +1 -0
- package/dist/cjs/errors.js +78 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/index.js +12 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/queries.js +466 -0
- package/dist/cjs/queries.js.map +1 -0
- package/dist/cjs/types.js +3 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/client.d.ts +87 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +151 -9
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +420 -0
- package/dist/mcp.js.map +1 -0
- package/package.json +18 -7
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.MonarchMoney = void 0;
|
|
40
|
+
const crypto = __importStar(require("node:crypto"));
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const path = __importStar(require("node:path"));
|
|
43
|
+
const readline = __importStar(require("node:readline"));
|
|
44
|
+
const speakeasy_1 = __importDefault(require("speakeasy"));
|
|
45
|
+
const endpoints_js_1 = require("./endpoints.js");
|
|
46
|
+
const errors_js_1 = require("./errors.js");
|
|
47
|
+
const queries = __importStar(require("./queries.js"));
|
|
48
|
+
const SESSION_FILE = ".mm/mm_session.json";
|
|
49
|
+
const DEFAULT_RECORD_LIMIT = 100;
|
|
50
|
+
const USER_AGENT = "MonarchMoneyAPI (https://github.com/hammem/monarchmoney)";
|
|
51
|
+
const ORIGIN = "https://app.monarch.com";
|
|
52
|
+
const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
|
|
53
|
+
const DEFAULT_RETRY_BASE_DELAY_MS = 500;
|
|
54
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
55
|
+
const DEFAULT_RATE_LIMIT_RPS = 0; // disabled
|
|
56
|
+
class MonarchMoney {
|
|
57
|
+
_headers;
|
|
58
|
+
_sessionFile;
|
|
59
|
+
_token;
|
|
60
|
+
_timeout;
|
|
61
|
+
_maxRetries;
|
|
62
|
+
_retryBaseDelayMs;
|
|
63
|
+
_rateLimitRps;
|
|
64
|
+
_rateLimitTokens;
|
|
65
|
+
_rateLimitLastRefill;
|
|
66
|
+
constructor(options = {}) {
|
|
67
|
+
const { sessionFile = SESSION_FILE, timeout = 10, token } = options;
|
|
68
|
+
this._headers = {
|
|
69
|
+
Accept: "application/json",
|
|
70
|
+
"Client-Platform": "web",
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
"User-Agent": USER_AGENT,
|
|
73
|
+
Origin: ORIGIN,
|
|
74
|
+
"device-uuid": crypto.randomUUID(),
|
|
75
|
+
"monarch-client": "monarch-core-web-app-graphql",
|
|
76
|
+
"monarch-client-version": "v1.0.1668",
|
|
77
|
+
};
|
|
78
|
+
if (token) {
|
|
79
|
+
this._headers["Authorization"] = `Token ${token}`;
|
|
80
|
+
}
|
|
81
|
+
this._sessionFile = path.isAbsolute(sessionFile)
|
|
82
|
+
? sessionFile
|
|
83
|
+
: path.resolve(process.cwd(), sessionFile);
|
|
84
|
+
this._token = token ?? null;
|
|
85
|
+
this._timeout = timeout * 1000;
|
|
86
|
+
this._maxRetries = options.retry?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
87
|
+
this._retryBaseDelayMs = options.retry?.baseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS;
|
|
88
|
+
this._rateLimitRps = options.rateLimit?.requestsPerSecond ?? DEFAULT_RATE_LIMIT_RPS;
|
|
89
|
+
this._rateLimitTokens = this._rateLimitRps || 1;
|
|
90
|
+
this._rateLimitLastRefill = Date.now();
|
|
91
|
+
}
|
|
92
|
+
/** Timeout for API calls, in seconds. */
|
|
93
|
+
get timeout() {
|
|
94
|
+
return this._timeout / 1000;
|
|
95
|
+
}
|
|
96
|
+
/** Sets the timeout for API calls, in seconds. */
|
|
97
|
+
setTimeout(timeoutSecs) {
|
|
98
|
+
this._timeout = timeoutSecs * 1000;
|
|
99
|
+
}
|
|
100
|
+
/** The current auth token, or `null` if not logged in. */
|
|
101
|
+
get token() {
|
|
102
|
+
return this._token;
|
|
103
|
+
}
|
|
104
|
+
/** Sets the auth token directly (e.g. from an external source). */
|
|
105
|
+
setToken(token) {
|
|
106
|
+
this._token = token;
|
|
107
|
+
this._headers["Authorization"] = `Token ${token}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Logs into Monarch Money.
|
|
111
|
+
*
|
|
112
|
+
* @param email - Account email address.
|
|
113
|
+
* @param password - Account password.
|
|
114
|
+
* @param options.useSavedSession - Load token from disk if available. Default: `true`.
|
|
115
|
+
* @param options.saveSession - Persist token to disk after login. Default: `true`.
|
|
116
|
+
* @param options.mfaSecretKey - TOTP secret for automatic MFA (base32). Bypasses MFA prompt.
|
|
117
|
+
* @throws {RequireMFAException} If MFA is required. Call `multiFactorAuthenticate()` next.
|
|
118
|
+
* @throws {LoginFailedException} If credentials are invalid.
|
|
119
|
+
*/
|
|
120
|
+
async login(email, password, options = {}) {
|
|
121
|
+
const { useSavedSession = true, saveSession = true, mfaSecretKey } = options;
|
|
122
|
+
if (useSavedSession && fs.existsSync(this._sessionFile)) {
|
|
123
|
+
this.loadSession(this._sessionFile);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!email?.trim() || !password?.trim()) {
|
|
127
|
+
throw new errors_js_1.LoginFailedException("Email and password are required to login when not using a saved session.");
|
|
128
|
+
}
|
|
129
|
+
await this._loginUser(email, password, mfaSecretKey);
|
|
130
|
+
if (saveSession) {
|
|
131
|
+
this.saveSession(this._sessionFile);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Completes the MFA step after a `RequireMFAException`.
|
|
136
|
+
*
|
|
137
|
+
* @param email - Account email address.
|
|
138
|
+
* @param password - Account password.
|
|
139
|
+
* @param code - The 6-digit TOTP code.
|
|
140
|
+
*/
|
|
141
|
+
async multiFactorAuthenticate(email, password, code) {
|
|
142
|
+
await this._multiFactorAuthenticate(email, password, code);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Submits the code sent to your email when the API returns "Retrieve the code from your email to continue login."
|
|
146
|
+
*/
|
|
147
|
+
async submitEmailOtp(email, password, code) {
|
|
148
|
+
await this._submitEmailOtp(email, password, code);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Interactive CLI login that prompts for email, password, and MFA code if needed.
|
|
152
|
+
*/
|
|
153
|
+
async interactiveLogin(options = {}) {
|
|
154
|
+
const { useSavedSession = true, saveSession = true } = options;
|
|
155
|
+
const rl = readline.createInterface({
|
|
156
|
+
input: process.stdin,
|
|
157
|
+
output: process.stdout,
|
|
158
|
+
});
|
|
159
|
+
const ask = (prompt) => new Promise((resolve) => rl.question(prompt, (answer) => resolve(answer.trim())));
|
|
160
|
+
let email = "";
|
|
161
|
+
let passwd = "";
|
|
162
|
+
try {
|
|
163
|
+
email = await ask("Email: ");
|
|
164
|
+
passwd = await ask("Password: ");
|
|
165
|
+
await this.login(email, passwd, { useSavedSession, saveSession: false });
|
|
166
|
+
if (saveSession)
|
|
167
|
+
this.saveSession(this._sessionFile);
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
if (e instanceof errors_js_1.EmailOtpRequiredException) {
|
|
171
|
+
const code = await ask("Enter the code from your email: ");
|
|
172
|
+
await this.submitEmailOtp(email, passwd, code);
|
|
173
|
+
if (saveSession)
|
|
174
|
+
this.saveSession(this._sessionFile);
|
|
175
|
+
}
|
|
176
|
+
else if (e instanceof errors_js_1.RequireMFAException) {
|
|
177
|
+
const code = await ask("Two Factor Code: ");
|
|
178
|
+
await this.multiFactorAuthenticate(email, passwd, code);
|
|
179
|
+
if (saveSession)
|
|
180
|
+
this.saveSession(this._sessionFile);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
throw e;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
rl.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Saves the current session token to disk. */
|
|
191
|
+
saveSession(filename) {
|
|
192
|
+
const file = path.resolve(filename ?? this._sessionFile);
|
|
193
|
+
const dir = path.dirname(file);
|
|
194
|
+
if (!fs.existsSync(dir))
|
|
195
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
196
|
+
fs.writeFileSync(file, JSON.stringify({ token: this._token }), {
|
|
197
|
+
encoding: "utf8",
|
|
198
|
+
mode: 0o600,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/** Loads a previously saved session token from disk. */
|
|
202
|
+
loadSession(filename) {
|
|
203
|
+
const file = path.resolve(filename ?? this._sessionFile);
|
|
204
|
+
const raw = fs.readFileSync(file, "utf8");
|
|
205
|
+
const data = JSON.parse(raw);
|
|
206
|
+
if (!data.token) {
|
|
207
|
+
throw new errors_js_1.LoginFailedException("Session file does not contain a valid token.");
|
|
208
|
+
}
|
|
209
|
+
this.setToken(data.token);
|
|
210
|
+
}
|
|
211
|
+
/** Deletes the session file from disk. */
|
|
212
|
+
deleteSession(filename) {
|
|
213
|
+
const file = path.resolve(filename ?? this._sessionFile);
|
|
214
|
+
if (fs.existsSync(file))
|
|
215
|
+
fs.unlinkSync(file);
|
|
216
|
+
}
|
|
217
|
+
// ---------- Private auth ----------
|
|
218
|
+
async _loginUser(email, password, mfaSecretKey) {
|
|
219
|
+
const body = {
|
|
220
|
+
username: email,
|
|
221
|
+
password,
|
|
222
|
+
supports_mfa: true,
|
|
223
|
+
supports_email_otp: true,
|
|
224
|
+
supports_recaptcha: true,
|
|
225
|
+
trusted_device: false,
|
|
226
|
+
};
|
|
227
|
+
if (mfaSecretKey) {
|
|
228
|
+
body.totp = speakeasy_1.default.totp({ secret: mfaSecretKey, encoding: "base32" });
|
|
229
|
+
}
|
|
230
|
+
const res = await fetch((0, endpoints_js_1.getLoginEndpoint)(), {
|
|
231
|
+
method: "POST",
|
|
232
|
+
headers: this._headers,
|
|
233
|
+
body: JSON.stringify(body),
|
|
234
|
+
signal: AbortSignal.timeout(this._timeout),
|
|
235
|
+
});
|
|
236
|
+
let bodyText = "";
|
|
237
|
+
try {
|
|
238
|
+
bodyText = await res.text();
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// ignore
|
|
242
|
+
}
|
|
243
|
+
if (!res.ok) {
|
|
244
|
+
let detail = "";
|
|
245
|
+
let errorCode = "";
|
|
246
|
+
try {
|
|
247
|
+
const data = JSON.parse(bodyText);
|
|
248
|
+
detail = typeof data.detail === "string" ? data.detail : "";
|
|
249
|
+
errorCode = typeof data.error_code === "string" ? data.error_code : "";
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
detail = bodyText || "";
|
|
253
|
+
}
|
|
254
|
+
const combined = `${detail} ${errorCode}`.toLowerCase();
|
|
255
|
+
if (errorCode === "EMAIL_OTP_REQUIRED" || (res.status === 403 && /email.*code|email.*otp|otp.*email/i.test(combined))) {
|
|
256
|
+
throw new errors_js_1.EmailOtpRequiredException(detail || "Email verification code required. Check your email.");
|
|
257
|
+
}
|
|
258
|
+
if (res.status === 403 && /mfa|multi.?factor|two.?factor|2fa|totp/i.test(combined)) {
|
|
259
|
+
throw new errors_js_1.RequireMFAException(detail || "Multi-Factor Auth Required");
|
|
260
|
+
}
|
|
261
|
+
throw new errors_js_1.LoginFailedException(detail || `HTTP ${res.status}: ${res.statusText}`, res.status);
|
|
262
|
+
}
|
|
263
|
+
const data = JSON.parse(bodyText);
|
|
264
|
+
this.setToken(data.token);
|
|
265
|
+
}
|
|
266
|
+
async _multiFactorAuthenticate(email, password, code) {
|
|
267
|
+
const body = {
|
|
268
|
+
username: email,
|
|
269
|
+
password,
|
|
270
|
+
supports_mfa: true,
|
|
271
|
+
totp: code,
|
|
272
|
+
trusted_device: false,
|
|
273
|
+
};
|
|
274
|
+
const res = await fetch((0, endpoints_js_1.getLoginEndpoint)(), {
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers: this._headers,
|
|
277
|
+
body: JSON.stringify(body),
|
|
278
|
+
signal: AbortSignal.timeout(this._timeout),
|
|
279
|
+
});
|
|
280
|
+
if (!res.ok) {
|
|
281
|
+
let msg = "";
|
|
282
|
+
try {
|
|
283
|
+
const data = (await res.json());
|
|
284
|
+
if (typeof data.detail === "string")
|
|
285
|
+
msg = data.detail;
|
|
286
|
+
else if (typeof data.error_code === "string")
|
|
287
|
+
msg = data.error_code;
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// response body not JSON
|
|
291
|
+
}
|
|
292
|
+
throw new errors_js_1.LoginFailedException(msg || `HTTP ${res.status}: ${res.statusText}`, res.status);
|
|
293
|
+
}
|
|
294
|
+
const data = (await res.json());
|
|
295
|
+
this.setToken(data.token);
|
|
296
|
+
}
|
|
297
|
+
async _submitEmailOtp(email, password, code) {
|
|
298
|
+
const body = {
|
|
299
|
+
username: email,
|
|
300
|
+
password,
|
|
301
|
+
supports_mfa: true,
|
|
302
|
+
supports_email_otp: true,
|
|
303
|
+
supports_recaptcha: true,
|
|
304
|
+
trusted_device: false,
|
|
305
|
+
email_otp: code,
|
|
306
|
+
};
|
|
307
|
+
const res = await fetch((0, endpoints_js_1.getLoginEndpoint)(), {
|
|
308
|
+
method: "POST",
|
|
309
|
+
headers: this._headers,
|
|
310
|
+
body: JSON.stringify(body),
|
|
311
|
+
signal: AbortSignal.timeout(this._timeout),
|
|
312
|
+
});
|
|
313
|
+
let bodyText = "";
|
|
314
|
+
try {
|
|
315
|
+
bodyText = await res.text();
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// ignore
|
|
319
|
+
}
|
|
320
|
+
if (!res.ok) {
|
|
321
|
+
let msg = "";
|
|
322
|
+
try {
|
|
323
|
+
const data = JSON.parse(bodyText);
|
|
324
|
+
if (typeof data.detail === "string")
|
|
325
|
+
msg = data.detail;
|
|
326
|
+
else if (typeof data.error_code === "string")
|
|
327
|
+
msg = data.error_code;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
msg = bodyText || "";
|
|
331
|
+
}
|
|
332
|
+
throw new errors_js_1.LoginFailedException(msg || `HTTP ${res.status}: ${res.statusText}`, res.status);
|
|
333
|
+
}
|
|
334
|
+
const data = JSON.parse(bodyText);
|
|
335
|
+
this.setToken(data.token);
|
|
336
|
+
}
|
|
337
|
+
// ---------- Rate limiter ----------
|
|
338
|
+
async _acquireRateLimitToken() {
|
|
339
|
+
if (this._rateLimitRps <= 0)
|
|
340
|
+
return;
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
const elapsed = now - this._rateLimitLastRefill;
|
|
343
|
+
const refill = (elapsed / 1000) * this._rateLimitRps;
|
|
344
|
+
this._rateLimitTokens = Math.min(this._rateLimitRps, this._rateLimitTokens + refill);
|
|
345
|
+
this._rateLimitLastRefill = now;
|
|
346
|
+
if (this._rateLimitTokens < 1) {
|
|
347
|
+
const waitMs = ((1 - this._rateLimitTokens) / this._rateLimitRps) * 1000;
|
|
348
|
+
await new Promise((r) => globalThis.setTimeout(r, waitMs));
|
|
349
|
+
this._rateLimitTokens = 0;
|
|
350
|
+
this._rateLimitLastRefill = Date.now();
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
this._rateLimitTokens -= 1;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// ---------- Retry with backoff ----------
|
|
357
|
+
async _fetchWithRetry(url, init) {
|
|
358
|
+
await this._acquireRateLimitToken();
|
|
359
|
+
let lastError;
|
|
360
|
+
for (let attempt = 0; attempt <= this._maxRetries; attempt++) {
|
|
361
|
+
try {
|
|
362
|
+
const res = await fetch(url, {
|
|
363
|
+
...init,
|
|
364
|
+
signal: AbortSignal.timeout(this._timeout),
|
|
365
|
+
});
|
|
366
|
+
if (res.ok || !RETRYABLE_STATUS_CODES.has(res.status) || attempt === this._maxRetries) {
|
|
367
|
+
return res;
|
|
368
|
+
}
|
|
369
|
+
const retryAfterHeader = res.headers.get("Retry-After");
|
|
370
|
+
const retryAfterMs = retryAfterHeader
|
|
371
|
+
? parseFloat(retryAfterHeader) * 1000
|
|
372
|
+
: undefined;
|
|
373
|
+
await this._backoff(attempt, retryAfterMs);
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
377
|
+
if (attempt === this._maxRetries)
|
|
378
|
+
break;
|
|
379
|
+
await this._backoff(attempt);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
throw lastError ?? new errors_js_1.RequestFailedException("Request failed after retries");
|
|
383
|
+
}
|
|
384
|
+
async _backoff(attempt, retryAfterMs) {
|
|
385
|
+
const jitter = Math.random() * 0.5 + 0.75; // 0.75–1.25x
|
|
386
|
+
const exponentialMs = this._retryBaseDelayMs * Math.pow(2, attempt) * jitter;
|
|
387
|
+
const delayMs = retryAfterMs != null
|
|
388
|
+
? Math.max(retryAfterMs, exponentialMs)
|
|
389
|
+
: exponentialMs;
|
|
390
|
+
await new Promise((r) => globalThis.setTimeout(r, delayMs));
|
|
391
|
+
}
|
|
392
|
+
// ---------- Private GraphQL ----------
|
|
393
|
+
async gqlCall(operation, query, variables = {}) {
|
|
394
|
+
if (!this._token) {
|
|
395
|
+
throw new errors_js_1.LoginFailedException("Not authenticated. Call login() first or provide a token.");
|
|
396
|
+
}
|
|
397
|
+
const res = await this._fetchWithRetry((0, endpoints_js_1.getGraphQL)(), {
|
|
398
|
+
method: "POST",
|
|
399
|
+
headers: this._headers,
|
|
400
|
+
body: JSON.stringify({ operationName: operation, query, variables }),
|
|
401
|
+
});
|
|
402
|
+
if (!res.ok) {
|
|
403
|
+
throw new errors_js_1.RequestFailedException(`HTTP ${res.status}: ${res.statusText}`, { statusCode: res.status });
|
|
404
|
+
}
|
|
405
|
+
const json = (await res.json());
|
|
406
|
+
if (json.errors?.length) {
|
|
407
|
+
throw new errors_js_1.RequestFailedException(json.errors.map((e) => e.message).join("; "), { graphQLErrors: json.errors });
|
|
408
|
+
}
|
|
409
|
+
if (!json.data) {
|
|
410
|
+
throw new errors_js_1.RequestFailedException("No data in GraphQL response");
|
|
411
|
+
}
|
|
412
|
+
return json.data;
|
|
413
|
+
}
|
|
414
|
+
// ---------- Date helpers ----------
|
|
415
|
+
_getStartOfCurrentMonth() {
|
|
416
|
+
const d = new Date();
|
|
417
|
+
return new Date(d.getFullYear(), d.getMonth(), 1).toISOString().slice(0, 10);
|
|
418
|
+
}
|
|
419
|
+
_getEndOfCurrentMonth() {
|
|
420
|
+
const d = new Date();
|
|
421
|
+
return new Date(d.getFullYear(), d.getMonth() + 1, 0)
|
|
422
|
+
.toISOString()
|
|
423
|
+
.slice(0, 10);
|
|
424
|
+
}
|
|
425
|
+
// =====================================================================
|
|
426
|
+
// READ METHODS
|
|
427
|
+
// =====================================================================
|
|
428
|
+
/** Gets all accounts linked to Monarch Money. */
|
|
429
|
+
async getAccounts() {
|
|
430
|
+
return this.gqlCall("GetAccounts", queries.GET_ACCOUNTS);
|
|
431
|
+
}
|
|
432
|
+
/** Gets all available account types and their subtypes. */
|
|
433
|
+
async getAccountTypeOptions() {
|
|
434
|
+
return this.gqlCall("GetAccountTypeOptions", queries.GET_ACCOUNT_TYPE_OPTIONS);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Gets daily account balances starting from `startDate`.
|
|
438
|
+
* Defaults to 31 days ago if not specified.
|
|
439
|
+
*/
|
|
440
|
+
async getRecentAccountBalances(startDate) {
|
|
441
|
+
if (!startDate) {
|
|
442
|
+
const d = new Date();
|
|
443
|
+
d.setDate(d.getDate() - 31);
|
|
444
|
+
startDate = d.toISOString().slice(0, 10);
|
|
445
|
+
}
|
|
446
|
+
return this.gqlCall("GetAccountRecentBalances", queries.GET_RECENT_ACCOUNT_BALANCES, { startDate });
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Gets net-value snapshots grouped by account type.
|
|
450
|
+
* @param timeframe - `"year"` or `"month"` granularity.
|
|
451
|
+
*/
|
|
452
|
+
async getAccountSnapshotsByType(startDate, timeframe) {
|
|
453
|
+
return this.gqlCall("GetSnapshotsByAccountType", queries.GET_SNAPSHOTS_BY_ACCOUNT_TYPE, { startDate, timeframe });
|
|
454
|
+
}
|
|
455
|
+
/** Gets daily aggregate net value across all accounts. */
|
|
456
|
+
async getAggregateSnapshots(options) {
|
|
457
|
+
const d = new Date();
|
|
458
|
+
d.setFullYear(d.getFullYear() - 150);
|
|
459
|
+
d.setDate(1);
|
|
460
|
+
return this.gqlCall("GetAggregateSnapshots", queries.GET_AGGREGATE_SNAPSHOTS, {
|
|
461
|
+
filters: {
|
|
462
|
+
startDate: options?.startDate ?? d.toISOString().slice(0, 10),
|
|
463
|
+
endDate: options?.endDate ?? undefined,
|
|
464
|
+
accountType: options?.accountType ?? undefined,
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
/** Gets holdings (securities) for a brokerage or investment account. */
|
|
469
|
+
async getAccountHoldings(accountId) {
|
|
470
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
471
|
+
return this.gqlCall("Web_GetHoldings", queries.GET_HOLDINGS, {
|
|
472
|
+
input: {
|
|
473
|
+
accountIds: [String(accountId)],
|
|
474
|
+
endDate: today,
|
|
475
|
+
includeHiddenHoldings: true,
|
|
476
|
+
startDate: today,
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
/** Gets daily balance history for a specific account. */
|
|
481
|
+
async getAccountHistory(accountId) {
|
|
482
|
+
const result = await this.gqlCall("AccountDetails_getAccount", queries.GET_ACCOUNT_HISTORY, {
|
|
483
|
+
id: String(accountId),
|
|
484
|
+
});
|
|
485
|
+
return result.snapshots.map((s) => ({
|
|
486
|
+
...s,
|
|
487
|
+
accountId: String(accountId),
|
|
488
|
+
accountName: result.account.displayName,
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
491
|
+
/** Gets linked institutions and their credentials. */
|
|
492
|
+
async getInstitutions() {
|
|
493
|
+
return this.gqlCall("Web_GetInstitutionSettings", queries.GET_INSTITUTIONS);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Gets budgets with actual amounts for the given date range.
|
|
497
|
+
* Defaults to previous month through next month.
|
|
498
|
+
*/
|
|
499
|
+
async getBudgets(startDate, endDate) {
|
|
500
|
+
let start = startDate;
|
|
501
|
+
let end = endDate;
|
|
502
|
+
if (!start && !end) {
|
|
503
|
+
const d = new Date();
|
|
504
|
+
start = new Date(d.getFullYear(), d.getMonth() - 1, 1)
|
|
505
|
+
.toISOString()
|
|
506
|
+
.slice(0, 10);
|
|
507
|
+
end = new Date(d.getFullYear(), d.getMonth() + 2, 0)
|
|
508
|
+
.toISOString()
|
|
509
|
+
.slice(0, 10);
|
|
510
|
+
}
|
|
511
|
+
else if (Boolean(start) !== Boolean(end)) {
|
|
512
|
+
throw new Error("You must specify both startDate and endDate, not just one of them.");
|
|
513
|
+
}
|
|
514
|
+
return this.gqlCall("Common_GetJointPlanningData", queries.GET_BUDGETS, { startDate: start, endDate: end });
|
|
515
|
+
}
|
|
516
|
+
/** Gets Monarch Money subscription details (plan status, trial, etc.). */
|
|
517
|
+
async getSubscriptionDetails() {
|
|
518
|
+
return this.gqlCall("GetSubscriptionDetails", queries.GET_SUBSCRIPTION_DETAILS);
|
|
519
|
+
}
|
|
520
|
+
/** Gets aggregate transaction summary (totals, averages, counts). */
|
|
521
|
+
async getTransactionsSummary() {
|
|
522
|
+
return this.gqlCall("GetTransactionsPage", queries.GET_TRANSACTIONS_SUMMARY);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Gets transactions with filtering, pagination, and sorting.
|
|
526
|
+
* Defaults to the most recent 100 transactions.
|
|
527
|
+
*/
|
|
528
|
+
async getTransactions(options = {}) {
|
|
529
|
+
const { limit = DEFAULT_RECORD_LIMIT, offset = 0, ...filterOpts } = options;
|
|
530
|
+
const filters = this._buildTransactionFilters(filterOpts);
|
|
531
|
+
return this.gqlCall("GetTransactionsList", queries.GET_TRANSACTIONS_LIST, { offset, limit, orderBy: "date", filters });
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Async generator that automatically paginates through all matching transactions.
|
|
535
|
+
* Yields one page of `Transaction[]` at a time.
|
|
536
|
+
*
|
|
537
|
+
* @param options - Same filter options as `getTransactions()`.
|
|
538
|
+
* @param options.pageSize - Number of transactions per page. Default: `100`.
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* ```ts
|
|
542
|
+
* for await (const page of mm.getTransactionPages({ startDate: "2025-01-01", endDate: "2025-12-31" })) {
|
|
543
|
+
* for (const tx of page) {
|
|
544
|
+
* console.log(tx.merchant?.name, tx.amount);
|
|
545
|
+
* }
|
|
546
|
+
* }
|
|
547
|
+
* ```
|
|
548
|
+
*/
|
|
549
|
+
async *getTransactionPages(options = {}) {
|
|
550
|
+
const { pageSize = DEFAULT_RECORD_LIMIT, ...filterOpts } = options;
|
|
551
|
+
let offset = 0;
|
|
552
|
+
while (true) {
|
|
553
|
+
const response = await this.getTransactions({
|
|
554
|
+
...filterOpts,
|
|
555
|
+
limit: pageSize,
|
|
556
|
+
offset,
|
|
557
|
+
});
|
|
558
|
+
const results = response.allTransactions.results;
|
|
559
|
+
if (results.length === 0)
|
|
560
|
+
break;
|
|
561
|
+
yield results;
|
|
562
|
+
offset += results.length;
|
|
563
|
+
if (offset >= response.allTransactions.totalCount)
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Returns all matching transactions across all pages as a flat array.
|
|
569
|
+
* Convenience wrapper around `getTransactionPages()`.
|
|
570
|
+
*/
|
|
571
|
+
async getAllTransactions(options = {}) {
|
|
572
|
+
const all = [];
|
|
573
|
+
for await (const page of this.getTransactionPages(options)) {
|
|
574
|
+
all.push(...page);
|
|
575
|
+
}
|
|
576
|
+
return all;
|
|
577
|
+
}
|
|
578
|
+
_buildTransactionFilters(opts) {
|
|
579
|
+
const { startDate, endDate, search = "", categoryIds = [], accountIds = [], tagIds = [], hasAttachments, hasNotes, hiddenFromReports, isSplit, isRecurring, importedFromMint, syncedFromInstitution, } = opts;
|
|
580
|
+
if (Boolean(startDate) !== Boolean(endDate)) {
|
|
581
|
+
throw new Error("You must specify both startDate and endDate, not just one.");
|
|
582
|
+
}
|
|
583
|
+
const filters = {
|
|
584
|
+
search,
|
|
585
|
+
categories: categoryIds,
|
|
586
|
+
accounts: accountIds,
|
|
587
|
+
tags: tagIds,
|
|
588
|
+
};
|
|
589
|
+
if (hasAttachments != null)
|
|
590
|
+
filters.hasAttachments = hasAttachments;
|
|
591
|
+
if (hasNotes != null)
|
|
592
|
+
filters.hasNotes = hasNotes;
|
|
593
|
+
if (hiddenFromReports != null)
|
|
594
|
+
filters.hideFromReports = hiddenFromReports;
|
|
595
|
+
if (isRecurring != null)
|
|
596
|
+
filters.isRecurring = isRecurring;
|
|
597
|
+
if (isSplit != null)
|
|
598
|
+
filters.isSplit = isSplit;
|
|
599
|
+
if (importedFromMint != null)
|
|
600
|
+
filters.importedFromMint = importedFromMint;
|
|
601
|
+
if (syncedFromInstitution != null)
|
|
602
|
+
filters.syncedFromInstitution = syncedFromInstitution;
|
|
603
|
+
if (startDate && endDate) {
|
|
604
|
+
filters.startDate = startDate;
|
|
605
|
+
filters.endDate = endDate;
|
|
606
|
+
}
|
|
607
|
+
return filters;
|
|
608
|
+
}
|
|
609
|
+
/** Gets all transaction categories. */
|
|
610
|
+
async getTransactionCategories() {
|
|
611
|
+
return this.gqlCall("GetCategories", queries.GET_CATEGORIES);
|
|
612
|
+
}
|
|
613
|
+
/** Gets all category groups. */
|
|
614
|
+
async getTransactionCategoryGroups() {
|
|
615
|
+
return this.gqlCall("ManageGetCategoryGroups", queries.GET_CATEGORY_GROUPS);
|
|
616
|
+
}
|
|
617
|
+
/** Gets detailed data for a single transaction. */
|
|
618
|
+
async getTransactionDetails(transactionId) {
|
|
619
|
+
return this.gqlCall("GetTransactionDetails", queries.GET_TRANSACTION_DETAILS, { id: transactionId });
|
|
620
|
+
}
|
|
621
|
+
/** Gets the splits for a transaction. */
|
|
622
|
+
async getTransactionSplits(transactionId) {
|
|
623
|
+
return this.gqlCall("GetTransactionSplits", queries.GET_TRANSACTION_SPLITS, { id: transactionId });
|
|
624
|
+
}
|
|
625
|
+
/** Gets all tags configured in the account. */
|
|
626
|
+
async getTransactionTags() {
|
|
627
|
+
return this.gqlCall("GetTransactionTags", queries.GET_TRANSACTION_TAGS);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Gets cashflow data grouped by category, category group, and merchant.
|
|
631
|
+
* Defaults to the current month.
|
|
632
|
+
*/
|
|
633
|
+
async getCashflow(options) {
|
|
634
|
+
if (options && Boolean(options.startDate) !== Boolean(options.endDate)) {
|
|
635
|
+
throw new Error("You must specify both startDate and endDate, not just one.");
|
|
636
|
+
}
|
|
637
|
+
const start = options?.startDate ?? this._getStartOfCurrentMonth();
|
|
638
|
+
const end = options?.endDate ?? this._getEndOfCurrentMonth();
|
|
639
|
+
return this.gqlCall("Web_GetCashFlowPage", queries.GET_CASHFLOW, {
|
|
640
|
+
filters: {
|
|
641
|
+
search: "",
|
|
642
|
+
categories: [],
|
|
643
|
+
accounts: [],
|
|
644
|
+
tags: [],
|
|
645
|
+
startDate: start,
|
|
646
|
+
endDate: end,
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Gets cashflow summary (income, expenses, savings, savings rate).
|
|
652
|
+
* Defaults to the current month.
|
|
653
|
+
*/
|
|
654
|
+
async getCashflowSummary(options) {
|
|
655
|
+
if (options && Boolean(options.startDate) !== Boolean(options.endDate)) {
|
|
656
|
+
throw new Error("You must specify both startDate and endDate, not just one.");
|
|
657
|
+
}
|
|
658
|
+
const start = options?.startDate ?? this._getStartOfCurrentMonth();
|
|
659
|
+
const end = options?.endDate ?? this._getEndOfCurrentMonth();
|
|
660
|
+
return this.gqlCall("Web_GetCashFlowPage", queries.GET_CASHFLOW_SUMMARY, {
|
|
661
|
+
filters: {
|
|
662
|
+
search: "",
|
|
663
|
+
categories: [],
|
|
664
|
+
accounts: [],
|
|
665
|
+
tags: [],
|
|
666
|
+
startDate: start,
|
|
667
|
+
endDate: end,
|
|
668
|
+
},
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Gets upcoming recurring transactions for a date range.
|
|
673
|
+
* Defaults to the current month.
|
|
674
|
+
*/
|
|
675
|
+
async getRecurringTransactions(startDate, endDate) {
|
|
676
|
+
if (Boolean(startDate) !== Boolean(endDate)) {
|
|
677
|
+
throw new Error("You must specify both startDate and endDate, not just one.");
|
|
678
|
+
}
|
|
679
|
+
return this.gqlCall("Web_GetUpcomingRecurringTransactionItems", queries.GET_RECURRING_TRANSACTIONS, {
|
|
680
|
+
startDate: startDate ?? this._getStartOfCurrentMonth(),
|
|
681
|
+
endDate: endDate ?? this._getEndOfCurrentMonth(),
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
/** Checks whether a prior account refresh request has completed. */
|
|
685
|
+
async isAccountsRefreshComplete(accountIds) {
|
|
686
|
+
const result = await this.gqlCall("ForceRefreshAccountsQuery", queries.GET_REFRESH_STATUS);
|
|
687
|
+
if (!result.accounts) {
|
|
688
|
+
throw new errors_js_1.RequestFailedException("Unable to check refresh status");
|
|
689
|
+
}
|
|
690
|
+
const list = accountIds?.length
|
|
691
|
+
? result.accounts.filter((a) => accountIds.includes(a.id))
|
|
692
|
+
: result.accounts;
|
|
693
|
+
return list.every((a) => !a.hasSyncInProgress);
|
|
694
|
+
}
|
|
695
|
+
// =====================================================================
|
|
696
|
+
// WRITE METHODS
|
|
697
|
+
// =====================================================================
|
|
698
|
+
/** Creates a new manual account. */
|
|
699
|
+
async createManualAccount(params) {
|
|
700
|
+
return this.gqlCall("Web_CreateManualAccount", queries.CREATE_MANUAL_ACCOUNT, {
|
|
701
|
+
input: {
|
|
702
|
+
type: params.accountType,
|
|
703
|
+
subtype: params.accountSubType,
|
|
704
|
+
includeInNetWorth: params.isInNetWorth,
|
|
705
|
+
name: params.accountName,
|
|
706
|
+
displayBalance: params.accountBalance ?? 0,
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
/** Updates an account's settings and/or balance. */
|
|
711
|
+
async updateAccount(accountId, updates) {
|
|
712
|
+
const input = { id: accountId };
|
|
713
|
+
if (updates.accountName != null)
|
|
714
|
+
input.name = updates.accountName;
|
|
715
|
+
if (updates.accountBalance != null)
|
|
716
|
+
input.displayBalance = updates.accountBalance;
|
|
717
|
+
if (updates.accountType != null)
|
|
718
|
+
input.type = updates.accountType;
|
|
719
|
+
if (updates.accountSubType != null)
|
|
720
|
+
input.subtype = updates.accountSubType;
|
|
721
|
+
if (updates.includeInNetWorth != null)
|
|
722
|
+
input.includeInNetWorth = updates.includeInNetWorth;
|
|
723
|
+
if (updates.hideFromSummaryList != null)
|
|
724
|
+
input.hideFromList = updates.hideFromSummaryList;
|
|
725
|
+
if (updates.hideTransactionsFromReports != null)
|
|
726
|
+
input.hideTransactionsFromReports = updates.hideTransactionsFromReports;
|
|
727
|
+
return this.gqlCall("Common_UpdateAccount", queries.UPDATE_ACCOUNT, { input });
|
|
728
|
+
}
|
|
729
|
+
/** Deletes an account by ID. */
|
|
730
|
+
async deleteAccount(accountId) {
|
|
731
|
+
return this.gqlCall("Common_DeleteAccount", queries.DELETE_ACCOUNT, { id: accountId });
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Requests an account balance/transaction refresh. Non-blocking.
|
|
735
|
+
* Use `isAccountsRefreshComplete()` to poll for status.
|
|
736
|
+
*/
|
|
737
|
+
async requestAccountsRefresh(accountIds) {
|
|
738
|
+
const result = await this.gqlCall("Common_ForceRefreshAccountsMutation", queries.FORCE_REFRESH_ACCOUNTS, { input: { accountIds } });
|
|
739
|
+
if (!result.forceRefreshAccounts.success) {
|
|
740
|
+
throw new errors_js_1.RequestFailedException(JSON.stringify(result.forceRefreshAccounts.errors));
|
|
741
|
+
}
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Refreshes accounts and polls until complete or timeout.
|
|
746
|
+
*
|
|
747
|
+
* @param options.onProgress - Called after each poll with progress info.
|
|
748
|
+
* @returns `true` if all accounts refreshed within the timeout, `false` otherwise.
|
|
749
|
+
*
|
|
750
|
+
* @example
|
|
751
|
+
* ```ts
|
|
752
|
+
* await mm.requestAccountsRefreshAndWait({
|
|
753
|
+
* onProgress: ({ completed, total, elapsedMs }) => {
|
|
754
|
+
* console.log(`${completed}/${total} accounts refreshed (${(elapsedMs / 1000).toFixed(0)}s)`);
|
|
755
|
+
* },
|
|
756
|
+
* });
|
|
757
|
+
* ```
|
|
758
|
+
*/
|
|
759
|
+
async requestAccountsRefreshAndWait(options) {
|
|
760
|
+
const { timeout = 300, delay = 10, onProgress } = options ?? {};
|
|
761
|
+
let accountIds = options?.accountIds;
|
|
762
|
+
if (!accountIds) {
|
|
763
|
+
const data = await this.getAccounts();
|
|
764
|
+
accountIds = data.accounts.map((a) => a.id);
|
|
765
|
+
}
|
|
766
|
+
await this.requestAccountsRefresh(accountIds);
|
|
767
|
+
const startTime = Date.now();
|
|
768
|
+
const deadline = startTime + timeout * 1000;
|
|
769
|
+
while (Date.now() < deadline) {
|
|
770
|
+
await new Promise((r) => globalThis.setTimeout(r, delay * 1000));
|
|
771
|
+
const result = await this.gqlCall("ForceRefreshAccountsQuery", queries.GET_REFRESH_STATUS);
|
|
772
|
+
if (!result.accounts) {
|
|
773
|
+
throw new errors_js_1.RequestFailedException("Unable to check refresh status");
|
|
774
|
+
}
|
|
775
|
+
const tracked = accountIds.length
|
|
776
|
+
? result.accounts.filter((a) => accountIds.includes(a.id))
|
|
777
|
+
: result.accounts;
|
|
778
|
+
const completed = tracked.filter((a) => !a.hasSyncInProgress).length;
|
|
779
|
+
onProgress?.({
|
|
780
|
+
completed,
|
|
781
|
+
total: tracked.length,
|
|
782
|
+
elapsedMs: Date.now() - startTime,
|
|
783
|
+
});
|
|
784
|
+
if (completed === tracked.length)
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
return false;
|
|
788
|
+
}
|
|
789
|
+
/** Creates a new transaction. */
|
|
790
|
+
async createTransaction(params) {
|
|
791
|
+
return this.gqlCall("Common_CreateTransactionMutation", queries.CREATE_TRANSACTION, {
|
|
792
|
+
input: {
|
|
793
|
+
date: params.date,
|
|
794
|
+
accountId: params.accountId,
|
|
795
|
+
amount: Math.round(params.amount * 100) / 100,
|
|
796
|
+
merchantName: params.merchantName,
|
|
797
|
+
categoryId: params.categoryId,
|
|
798
|
+
notes: params.notes ?? "",
|
|
799
|
+
shouldUpdateBalance: params.updateBalance ?? false,
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Updates an existing transaction. Only provided fields are changed.
|
|
805
|
+
*/
|
|
806
|
+
async updateTransaction(transactionId, updates) {
|
|
807
|
+
const input = { id: transactionId };
|
|
808
|
+
if (updates.categoryId != null)
|
|
809
|
+
input.category = updates.categoryId;
|
|
810
|
+
if (updates.merchantName != null)
|
|
811
|
+
input.name = updates.merchantName;
|
|
812
|
+
if (updates.goalId != null)
|
|
813
|
+
input.goalId = updates.goalId;
|
|
814
|
+
if (updates.amount != null)
|
|
815
|
+
input.amount = updates.amount;
|
|
816
|
+
if (updates.date != null)
|
|
817
|
+
input.date = updates.date;
|
|
818
|
+
if (updates.hideFromReports != null)
|
|
819
|
+
input.hideFromReports = updates.hideFromReports;
|
|
820
|
+
if (updates.needsReview != null)
|
|
821
|
+
input.needsReview = updates.needsReview;
|
|
822
|
+
if (updates.notes != null)
|
|
823
|
+
input.notes = updates.notes;
|
|
824
|
+
return this.gqlCall("Web_TransactionDrawerUpdateTransaction", queries.UPDATE_TRANSACTION, { input });
|
|
825
|
+
}
|
|
826
|
+
/** Deletes a transaction by ID. */
|
|
827
|
+
async deleteTransaction(transactionId) {
|
|
828
|
+
const result = await this.gqlCall("Common_DeleteTransactionMutation", queries.DELETE_TRANSACTION, { input: { id: transactionId } });
|
|
829
|
+
if (result.deleteTransaction.errors?.length) {
|
|
830
|
+
throw new errors_js_1.RequestFailedException(JSON.stringify(result.deleteTransaction.errors));
|
|
831
|
+
}
|
|
832
|
+
return result.deleteTransaction.deleted;
|
|
833
|
+
}
|
|
834
|
+
/** Deletes a transaction category. Optionally moves transactions to another category. */
|
|
835
|
+
async deleteTransactionCategory(categoryId, moveToCategoryId) {
|
|
836
|
+
const result = await this.gqlCall("Web_DeleteCategory", queries.DELETE_CATEGORY, { id: categoryId, moveToCategoryId });
|
|
837
|
+
if (!result.deleteCategory.deleted &&
|
|
838
|
+
result.deleteCategory.errors?.length) {
|
|
839
|
+
throw new errors_js_1.RequestFailedException(JSON.stringify(result.deleteCategory.errors));
|
|
840
|
+
}
|
|
841
|
+
return result.deleteCategory.deleted;
|
|
842
|
+
}
|
|
843
|
+
/** Deletes multiple transaction categories. Returns results per category. */
|
|
844
|
+
async deleteTransactionCategories(categoryIds) {
|
|
845
|
+
return Promise.all(categoryIds.map((id) => this.deleteTransactionCategory(id).catch((e) => e instanceof Error ? e : new Error(String(e)))));
|
|
846
|
+
}
|
|
847
|
+
/** Creates a new transaction category within a category group. */
|
|
848
|
+
async createTransactionCategory(params) {
|
|
849
|
+
const d = new Date();
|
|
850
|
+
d.setDate(1);
|
|
851
|
+
return this.gqlCall("Web_CreateCategory", queries.CREATE_CATEGORY, {
|
|
852
|
+
input: {
|
|
853
|
+
groupId: params.groupId,
|
|
854
|
+
name: params.name,
|
|
855
|
+
icon: params.icon ?? "\u2753",
|
|
856
|
+
rolloverStartMonth: params.rolloverStartMonth ?? d.toISOString().slice(0, 10),
|
|
857
|
+
rolloverEnabled: params.rolloverEnabled ?? false,
|
|
858
|
+
rolloverType: params.rolloverType ?? "monthly",
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
/** Creates a new tag for transactions. */
|
|
863
|
+
async createTransactionTag(name, color) {
|
|
864
|
+
return this.gqlCall("Common_CreateTransactionTag", queries.CREATE_TRANSACTION_TAG, { input: { name, color } });
|
|
865
|
+
}
|
|
866
|
+
/** Sets (replaces) all tags on a transaction. */
|
|
867
|
+
async setTransactionTags(transactionId, tagIds) {
|
|
868
|
+
return this.gqlCall("Web_SetTransactionTags", queries.SET_TRANSACTION_TAGS, { input: { transactionId, tagIds } });
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Creates, modifies, or removes splits on a transaction.
|
|
872
|
+
* Pass an empty array to remove all splits.
|
|
873
|
+
* The sum of split amounts must equal the transaction amount.
|
|
874
|
+
*/
|
|
875
|
+
async updateTransactionSplits(transactionId, splitData) {
|
|
876
|
+
return this.gqlCall("Common_SplitTransactionMutation", queries.SPLIT_TRANSACTION, { input: { transactionId, splitData } });
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Sets a budget amount for a category or category group.
|
|
880
|
+
* A zero amount clears the budget. Exactly one of `categoryId` or `categoryGroupId` is required.
|
|
881
|
+
*/
|
|
882
|
+
async setBudgetAmount(params) {
|
|
883
|
+
const { categoryId, categoryGroupId } = params;
|
|
884
|
+
if ((categoryId == null) === (categoryGroupId == null)) {
|
|
885
|
+
throw new Error("You must specify either categoryId OR categoryGroupId; not both.");
|
|
886
|
+
}
|
|
887
|
+
return this.gqlCall("Common_UpdateBudgetItem", queries.UPDATE_BUDGET_ITEM, {
|
|
888
|
+
input: {
|
|
889
|
+
startDate: params.startDate ?? this._getStartOfCurrentMonth(),
|
|
890
|
+
timeframe: params.timeframe ?? "month",
|
|
891
|
+
categoryId: categoryId ?? undefined,
|
|
892
|
+
categoryGroupId: categoryGroupId ?? undefined,
|
|
893
|
+
amount: params.amount,
|
|
894
|
+
applyToFuture: params.applyToFuture ?? false,
|
|
895
|
+
},
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Uploads a CSV file of balance history for a manual account.
|
|
900
|
+
* @param accountId - The account to apply history to.
|
|
901
|
+
* @param csvContent - CSV string content.
|
|
902
|
+
*/
|
|
903
|
+
async uploadAccountBalanceHistory(accountId, csvContent) {
|
|
904
|
+
if (!accountId?.trim() || !csvContent?.trim()) {
|
|
905
|
+
throw new errors_js_1.RequestFailedException("accountId and csvContent cannot be empty");
|
|
906
|
+
}
|
|
907
|
+
const form = new FormData();
|
|
908
|
+
form.append("files", new Blob([csvContent], { type: "text/csv" }), "upload.csv");
|
|
909
|
+
form.append("account_files_mapping", JSON.stringify({ "upload.csv": accountId }));
|
|
910
|
+
const headers = { ...this._headers };
|
|
911
|
+
delete headers["Content-Type"];
|
|
912
|
+
const res = await this._fetchWithRetry((0, endpoints_js_1.getAccountBalanceHistoryUploadEndpoint)(), {
|
|
913
|
+
method: "POST",
|
|
914
|
+
headers,
|
|
915
|
+
body: form,
|
|
916
|
+
});
|
|
917
|
+
if (!res.ok) {
|
|
918
|
+
throw new errors_js_1.RequestFailedException(`HTTP ${res.status}: ${res.statusText}`, { statusCode: res.status });
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
exports.MonarchMoney = MonarchMoney;
|
|
923
|
+
//# sourceMappingURL=client.js.map
|