@slothmoney/agent-cli 0.2.0 → 0.3.1
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/CHANGELOG.md +29 -0
- package/README.md +175 -38
- package/dist/args.js +302 -2
- package/dist/cli.js +615 -7
- package/dist/contracts.js +120 -3
- package/package.json +1 -1
package/dist/contracts.js
CHANGED
|
@@ -2,6 +2,12 @@ import { ApiError, UsageError, } from './errors.js';
|
|
|
2
2
|
function isObject(value) {
|
|
3
3
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
4
4
|
}
|
|
5
|
+
function isIsoDate(value) {
|
|
6
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value))
|
|
7
|
+
return false;
|
|
8
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
9
|
+
return !Number.isNaN(parsed.valueOf()) && parsed.toISOString().slice(0, 10) === value;
|
|
10
|
+
}
|
|
5
11
|
function requireObject(value, label) {
|
|
6
12
|
if (!isObject(value))
|
|
7
13
|
throw new UsageError(`${label} must be an object`);
|
|
@@ -38,12 +44,18 @@ function validateAssignment(value, index) {
|
|
|
38
44
|
const assignment = requireObject(value, label);
|
|
39
45
|
rejectUnknownFields(assignment, new Set([
|
|
40
46
|
'transactionRef',
|
|
47
|
+
'assignmentScope',
|
|
41
48
|
'categoryId',
|
|
42
49
|
'lineItemId',
|
|
43
50
|
'categorySplits',
|
|
44
51
|
'incomeSubtype',
|
|
45
52
|
]), label);
|
|
46
53
|
requireString(assignment.transactionRef, `${label}.transactionRef`);
|
|
54
|
+
if (assignment.assignmentScope !== undefined
|
|
55
|
+
&& assignment.assignmentScope !== 'personal'
|
|
56
|
+
&& assignment.assignmentScope !== 'joint') {
|
|
57
|
+
throw new UsageError(`${label}.assignmentScope must be personal or joint`);
|
|
58
|
+
}
|
|
47
59
|
if (assignment.categoryId !== undefined && assignment.categoryId !== null) {
|
|
48
60
|
requireString(assignment.categoryId, `${label}.categoryId`);
|
|
49
61
|
}
|
|
@@ -74,6 +86,9 @@ function validateAssignment(value, index) {
|
|
|
74
86
|
}
|
|
75
87
|
return {
|
|
76
88
|
transactionRef: assignment.transactionRef,
|
|
89
|
+
...(assignment.assignmentScope !== undefined
|
|
90
|
+
? { assignmentScope: assignment.assignmentScope }
|
|
91
|
+
: {}),
|
|
77
92
|
...(assignment.categoryId !== undefined ? { categoryId: assignment.categoryId } : {}),
|
|
78
93
|
...(assignment.lineItemId !== undefined ? { lineItemId: assignment.lineItemId } : {}),
|
|
79
94
|
...(categorySplits !== undefined ? { categorySplits } : {}),
|
|
@@ -130,25 +145,72 @@ function isTransaction(value) {
|
|
|
130
145
|
&& (value.categoryId === null || typeof value.categoryId === 'string')
|
|
131
146
|
&& (value.lineItemId === null || typeof value.lineItemId === 'string')
|
|
132
147
|
&& Array.isArray(value.categorySplits)
|
|
148
|
+
&& (value.jointBudgetContribution === null
|
|
149
|
+
|| (isObject(value.jointBudgetContribution)
|
|
150
|
+
&& typeof value.jointBudgetContribution.eligible === 'boolean'
|
|
151
|
+
&& typeof value.jointBudgetContribution.included === 'boolean'
|
|
152
|
+
&& Number.isInteger(value.jointBudgetContribution.amountPence)
|
|
153
|
+
&& Number(value.jointBudgetContribution.amountPence) > 0
|
|
154
|
+
&& (value.jointBudgetContribution.categoryId === null
|
|
155
|
+
|| typeof value.jointBudgetContribution.categoryId === 'string')
|
|
156
|
+
&& (value.jointBudgetContribution.lineItemId === null
|
|
157
|
+
|| typeof value.jointBudgetContribution.lineItemId === 'string')
|
|
158
|
+
&& Array.isArray(value.jointBudgetContribution.categorySplits)
|
|
159
|
+
&& (value.jointBudgetContribution.incomeSubtype === null
|
|
160
|
+
|| value.jointBudgetContribution.incomeSubtype === 'pay'
|
|
161
|
+
|| value.jointBudgetContribution.incomeSubtype === 'interest')))
|
|
133
162
|
&& (value.incomeSubtype === null
|
|
134
163
|
|| value.incomeSubtype === 'pay'
|
|
135
164
|
|| value.incomeSubtype === 'interest'));
|
|
136
165
|
}
|
|
137
166
|
function isTransactionsResponse(value) {
|
|
167
|
+
const validStatuses = new Set(['skipped', 'completed', 'in_progress', 'partial', 'failed']);
|
|
168
|
+
const validReasons = new Set([
|
|
169
|
+
'all_fetched_today',
|
|
170
|
+
'no_api_connections',
|
|
171
|
+
'no_selected_accounts',
|
|
172
|
+
'refreshed',
|
|
173
|
+
'wait_timeout',
|
|
174
|
+
'account_failures',
|
|
175
|
+
'refresh_error',
|
|
176
|
+
]);
|
|
177
|
+
const refresh = isObject(value) ? value.refresh : undefined;
|
|
138
178
|
return (isObject(value)
|
|
139
179
|
&& Array.isArray(value.transactions)
|
|
140
180
|
&& value.transactions.every(isTransaction)
|
|
141
|
-
&& (value.nextCursor === null || typeof value.nextCursor === 'string')
|
|
181
|
+
&& (value.nextCursor === null || typeof value.nextCursor === 'string')
|
|
182
|
+
&& isObject(refresh)
|
|
183
|
+
&& typeof refresh.status === 'string'
|
|
184
|
+
&& validStatuses.has(refresh.status)
|
|
185
|
+
&& typeof refresh.reason === 'string'
|
|
186
|
+
&& validReasons.has(refresh.reason)
|
|
187
|
+
&& isIsoDate(refresh.utcDate));
|
|
142
188
|
}
|
|
143
189
|
function isAssignmentResponse(value) {
|
|
144
190
|
return (isObject(value)
|
|
145
191
|
&& Array.isArray(value.succeeded)
|
|
146
|
-
&& value.succeeded.every((item) => isObject(item)
|
|
192
|
+
&& value.succeeded.every((item) => (isObject(item)
|
|
193
|
+
&& typeof item.transactionRef === 'string'
|
|
194
|
+
&& (item.assignmentScope === 'personal' || item.assignmentScope === 'joint')))
|
|
147
195
|
&& Array.isArray(value.failed)
|
|
148
196
|
&& value.failed.every((item) => (isObject(item)
|
|
149
197
|
&& typeof item.error === 'string'
|
|
150
198
|
&& (item.transactionRef === undefined || typeof item.transactionRef === 'string'))));
|
|
151
199
|
}
|
|
200
|
+
export function validateJointBudgetSettingsResponse(value) {
|
|
201
|
+
if (!isObject(value)
|
|
202
|
+
|| Object.keys(value).some((key) => !new Set([
|
|
203
|
+
'includeSharedPersonalTransactions',
|
|
204
|
+
'updatedAt',
|
|
205
|
+
'updatedBy',
|
|
206
|
+
]).has(key))
|
|
207
|
+
|| typeof value.includeSharedPersonalTransactions !== 'boolean'
|
|
208
|
+
|| (value.updatedAt !== null && !isIsoDateTime(value.updatedAt))
|
|
209
|
+
|| (value.updatedBy !== null && typeof value.updatedBy !== 'string')) {
|
|
210
|
+
throw new ApiError('Invalid joint budget settings response from the Agent API');
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
152
214
|
function isHttpUrl(value) {
|
|
153
215
|
if (typeof value !== 'string')
|
|
154
216
|
return false;
|
|
@@ -175,6 +237,55 @@ function isPartnerResponse(value) {
|
|
|
175
237
|
&& isIsoDateTime(value.expiresAt)
|
|
176
238
|
&& value.status === 'open');
|
|
177
239
|
}
|
|
240
|
+
function hasOnlyFields(value, fields) {
|
|
241
|
+
const allowed = new Set(fields);
|
|
242
|
+
return Object.keys(value).every((key) => allowed.has(key));
|
|
243
|
+
}
|
|
244
|
+
function isGoal(value) {
|
|
245
|
+
return (isObject(value)
|
|
246
|
+
&& hasOnlyFields(value, [
|
|
247
|
+
'id',
|
|
248
|
+
'name',
|
|
249
|
+
'targetAmount',
|
|
250
|
+
'targetMonthKey',
|
|
251
|
+
'isAchieved',
|
|
252
|
+
'sharedWithPartner',
|
|
253
|
+
])
|
|
254
|
+
&& typeof value.id === 'string'
|
|
255
|
+
&& value.id.trim().length > 0
|
|
256
|
+
&& typeof value.name === 'string'
|
|
257
|
+
&& value.name.trim().length > 0
|
|
258
|
+
&& (value.targetAmount === null
|
|
259
|
+
|| (typeof value.targetAmount === 'number' && Number.isFinite(value.targetAmount)))
|
|
260
|
+
&& (value.targetMonthKey === null
|
|
261
|
+
|| (typeof value.targetMonthKey === 'string'
|
|
262
|
+
&& /^\d{4}-(0[1-9]|1[0-2])$/.test(value.targetMonthKey)))
|
|
263
|
+
&& typeof value.isAchieved === 'boolean'
|
|
264
|
+
&& typeof value.sharedWithPartner === 'boolean');
|
|
265
|
+
}
|
|
266
|
+
function isCurrency(value) {
|
|
267
|
+
return typeof value === 'string' && /^[A-Z]{3}$/.test(value);
|
|
268
|
+
}
|
|
269
|
+
function isGoalsResponse(value) {
|
|
270
|
+
return (isObject(value)
|
|
271
|
+
&& hasOnlyFields(value, ['currency', 'goals'])
|
|
272
|
+
&& isCurrency(value.currency)
|
|
273
|
+
&& Array.isArray(value.goals)
|
|
274
|
+
&& value.goals.every(isGoal));
|
|
275
|
+
}
|
|
276
|
+
function isGoalMutationResponse(value) {
|
|
277
|
+
return (isObject(value)
|
|
278
|
+
&& hasOnlyFields(value, ['currency', 'goal'])
|
|
279
|
+
&& isCurrency(value.currency)
|
|
280
|
+
&& isGoal(value.goal));
|
|
281
|
+
}
|
|
282
|
+
function isGoalDeleteResponse(value) {
|
|
283
|
+
return (isObject(value)
|
|
284
|
+
&& hasOnlyFields(value, ['deleted', 'deletedGoalId'])
|
|
285
|
+
&& value.deleted === true
|
|
286
|
+
&& typeof value.deletedGoalId === 'string'
|
|
287
|
+
&& value.deletedGoalId.trim().length > 0);
|
|
288
|
+
}
|
|
178
289
|
export function parseApiResponse(command, value) {
|
|
179
290
|
const valid = command === 'categories'
|
|
180
291
|
? isCategoryResponse(value)
|
|
@@ -182,7 +293,13 @@ export function parseApiResponse(command, value) {
|
|
|
182
293
|
? isTransactionsResponse(value)
|
|
183
294
|
: command === 'assign'
|
|
184
295
|
? isAssignmentResponse(value)
|
|
185
|
-
:
|
|
296
|
+
: command === 'ask-partner'
|
|
297
|
+
? isPartnerResponse(value)
|
|
298
|
+
: command === 'goals-list'
|
|
299
|
+
? isGoalsResponse(value)
|
|
300
|
+
: command === 'goals-delete'
|
|
301
|
+
? isGoalDeleteResponse(value)
|
|
302
|
+
: isGoalMutationResponse(value);
|
|
186
303
|
if (!valid) {
|
|
187
304
|
const label = command === 'assign' ? 'assignment' : command;
|
|
188
305
|
throw new ApiError(`Invalid ${label} response from the Agent API`);
|