@remit/account-worker 0.0.19 → 0.0.21
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/account-worker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -48,19 +48,8 @@
|
|
|
48
48
|
"@types/aws-lambda": "*"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@aws-sdk/client-cloudfront": "*",
|
|
52
|
-
"@aws-sdk/client-cognito-identity-provider": "*",
|
|
53
|
-
"@aws-sdk/client-dynamodb": "*",
|
|
54
|
-
"@aws-sdk/client-s3": "*",
|
|
55
51
|
"@aws-sdk/core": "*",
|
|
56
|
-
"
|
|
57
|
-
"@aws-sdk/lib-storage": "*",
|
|
58
|
-
"@aws-sdk/s3-request-presigner": "*",
|
|
59
|
-
"@remit/domain-enums": "*",
|
|
60
|
-
"@remit/search-service": "*",
|
|
61
|
-
"aws-sdk-client-mock": "*",
|
|
62
|
-
"esbuild": "^0.28.1",
|
|
63
|
-
"p-map": "*"
|
|
52
|
+
"esbuild": "^0.28.1"
|
|
64
53
|
},
|
|
65
54
|
"license": "MIT",
|
|
66
55
|
"publishConfig": {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import type { RemitClient } from "@remit/backend/client";
|
|
4
|
+
import type { OrganizeMatchDeps } from "@remit/backend/organize";
|
|
5
|
+
import type { Logger } from "@remit/logger-lambda";
|
|
6
|
+
import type { OrganizeJobEvent } from "../events.js";
|
|
7
|
+
import { processOrganizeJob } from "./organize-job.js";
|
|
8
|
+
|
|
9
|
+
const noopLog = {
|
|
10
|
+
info: () => {},
|
|
11
|
+
warn: () => {},
|
|
12
|
+
error: () => {},
|
|
13
|
+
debug: () => {},
|
|
14
|
+
fatal: () => {},
|
|
15
|
+
trace: () => {},
|
|
16
|
+
child: () => noopLog,
|
|
17
|
+
} as unknown as Logger;
|
|
18
|
+
|
|
19
|
+
interface Update {
|
|
20
|
+
state: string;
|
|
21
|
+
errorMessage?: string;
|
|
22
|
+
matchedCount?: number;
|
|
23
|
+
appliedCount?: number;
|
|
24
|
+
failedCount?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const event: OrganizeJobEvent = {
|
|
28
|
+
type: "OrganizeJob",
|
|
29
|
+
accountConfigId: "cfg-1",
|
|
30
|
+
organizeJobId: "job-1",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const jobClient = (
|
|
34
|
+
updates: Update[],
|
|
35
|
+
literalClauses: Array<{ field: string; value: string }>,
|
|
36
|
+
): RemitClient =>
|
|
37
|
+
({
|
|
38
|
+
organizeJobRequest: {
|
|
39
|
+
get: async () => ({
|
|
40
|
+
organizeJobId: "job-1",
|
|
41
|
+
accountConfigId: "cfg-1",
|
|
42
|
+
anchorMessageId: "None",
|
|
43
|
+
matchOperator: "And",
|
|
44
|
+
literalClauses,
|
|
45
|
+
similarityThreshold: 0.75,
|
|
46
|
+
actionLabelId: "lbl-1",
|
|
47
|
+
actionMailboxId: "None",
|
|
48
|
+
}),
|
|
49
|
+
update: async (_id: string, patch: Update) => {
|
|
50
|
+
updates.push(patch);
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}) as unknown as RemitClient;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Matcher deps whose corpus read is a landmine: reaching it at all means the
|
|
57
|
+
* predicate was not refused up front, and the thrown error stands in for the
|
|
58
|
+
* infrastructure-class failure a retry exists for.
|
|
59
|
+
*/
|
|
60
|
+
const explodingMatchDeps = (): OrganizeMatchDeps =>
|
|
61
|
+
({
|
|
62
|
+
semantic: () => {
|
|
63
|
+
throw new Error("the vector pipeline must not be reached");
|
|
64
|
+
},
|
|
65
|
+
listAccountFilterMessages: async () => {
|
|
66
|
+
throw new Error("SQLITE_BUSY");
|
|
67
|
+
},
|
|
68
|
+
filterAnchors: {
|
|
69
|
+
listByAccountConfig: async () => [],
|
|
70
|
+
put: async () => {
|
|
71
|
+
throw new Error("unreachable");
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
}) as unknown as OrganizeMatchDeps;
|
|
75
|
+
|
|
76
|
+
describe("processOrganizeJob (reader #463)", () => {
|
|
77
|
+
it("fails the job on a refused rule and acknowledges the record", async () => {
|
|
78
|
+
const updates: Update[] = [];
|
|
79
|
+
|
|
80
|
+
await processOrganizeJob(event, noopLog, {
|
|
81
|
+
client: jobClient(updates, [{ field: "HasWords", value: "invoice" }]),
|
|
82
|
+
matchDeps: explodingMatchDeps(),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
assert.deepEqual(
|
|
86
|
+
updates.map((u) => u.state),
|
|
87
|
+
["Running", "Failed"],
|
|
88
|
+
);
|
|
89
|
+
const failed = updates[1];
|
|
90
|
+
assert.match(String(failed?.errorMessage), /HasWords/);
|
|
91
|
+
assert.equal(failed?.matchedCount, 0);
|
|
92
|
+
assert.equal(failed?.appliedCount, 0);
|
|
93
|
+
assert.equal(failed?.failedCount, 0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("fails the job and propagates an infrastructure failure so the record retries", async () => {
|
|
97
|
+
const updates: Update[] = [];
|
|
98
|
+
|
|
99
|
+
await assert.rejects(
|
|
100
|
+
processOrganizeJob(event, noopLog, {
|
|
101
|
+
client: jobClient(updates, [
|
|
102
|
+
{ field: "Subject", value: "reservation" },
|
|
103
|
+
]),
|
|
104
|
+
matchDeps: explodingMatchDeps(),
|
|
105
|
+
}),
|
|
106
|
+
/SQLITE_BUSY/,
|
|
107
|
+
"an SQS/DDB-class failure must stay loud so partial batch failure redelivers it",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
assert.deepEqual(
|
|
111
|
+
updates.map((u) => u.state),
|
|
112
|
+
["Running", "Failed"],
|
|
113
|
+
);
|
|
114
|
+
assert.equal(updates[1]?.errorMessage, "SQLITE_BUSY");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
import { getClient } from "@remit/backend/client";
|
|
1
|
+
import { getClient, type RemitClient } from "@remit/backend/client";
|
|
2
2
|
import {
|
|
3
3
|
applyOrganize,
|
|
4
4
|
buildOrganizeMatchDeps,
|
|
5
5
|
buildOrganizeMoveService,
|
|
6
6
|
matchOrganize,
|
|
7
7
|
ORGANIZE_MATCH_LIMIT,
|
|
8
|
+
type OrganizeMatchDeps,
|
|
8
9
|
predicateFromJob,
|
|
9
10
|
} from "@remit/backend/organize";
|
|
10
11
|
import type { Logger } from "@remit/logger-lambda";
|
|
11
12
|
import type { OrganizeJobEvent } from "../events.js";
|
|
12
13
|
|
|
14
|
+
export interface ProcessOrganizeJobDeps {
|
|
15
|
+
client?: RemitClient;
|
|
16
|
+
matchDeps?: OrganizeMatchDeps;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
/**
|
|
14
20
|
* Run a "all like these" back-apply job (RFC 034, #1278): match the corpus
|
|
15
21
|
* against the job's snapshotted predicate and apply the action to every match,
|
|
@@ -22,13 +28,22 @@ import type { OrganizeJobEvent } from "../events.js";
|
|
|
22
28
|
* additive label upsert and the exclusive folder move, the latter through the
|
|
23
29
|
* same local-first placement mover body sync uses (`buildOrganizeMoveService`),
|
|
24
30
|
* so a redelivered job re-applies both idempotently.
|
|
31
|
+
*
|
|
32
|
+
* Two failures, two treatments (reader #463). A predicate this deployment's
|
|
33
|
+
* matcher refuses is a rejected client input: the job row records the reason and
|
|
34
|
+
* the record is acknowledged, because redelivering the same snapshotted
|
|
35
|
+
* predicate can only be refused again — retrying it to the DLQ buries a 4xx in
|
|
36
|
+
* the infrastructure alarms. Everything else — the SQS/DDB/S3-class failure a
|
|
37
|
+
* retry can actually clear — fails the row and propagates, so partial batch
|
|
38
|
+
* failure redelivers it.
|
|
25
39
|
*/
|
|
26
40
|
export const processOrganizeJob = async (
|
|
27
41
|
event: OrganizeJobEvent,
|
|
28
42
|
log: Logger,
|
|
43
|
+
deps: ProcessOrganizeJobDeps = {},
|
|
29
44
|
): Promise<void> => {
|
|
30
45
|
const { accountConfigId, organizeJobId } = event;
|
|
31
|
-
const client = await getClient();
|
|
46
|
+
const client = deps.client ?? (await getClient());
|
|
32
47
|
|
|
33
48
|
const job = await client.organizeJobRequest.get(organizeJobId);
|
|
34
49
|
await client.organizeJobRequest.update(organizeJobId, { state: "Running" });
|
|
@@ -39,13 +54,30 @@ export const processOrganizeJob = async (
|
|
|
39
54
|
|
|
40
55
|
try {
|
|
41
56
|
const predicate = predicateFromJob(job);
|
|
42
|
-
const matchDeps = buildOrganizeMatchDeps(client);
|
|
43
|
-
const
|
|
57
|
+
const matchDeps = deps.matchDeps ?? buildOrganizeMatchDeps(client);
|
|
58
|
+
const match = await matchOrganize(
|
|
44
59
|
matchDeps,
|
|
45
60
|
accountConfigId,
|
|
46
61
|
predicate,
|
|
47
62
|
ORGANIZE_MATCH_LIMIT,
|
|
48
63
|
);
|
|
64
|
+
|
|
65
|
+
if (match.rejected) {
|
|
66
|
+
await client.organizeJobRequest.update(organizeJobId, {
|
|
67
|
+
state: "Failed",
|
|
68
|
+
matchedCount: 0,
|
|
69
|
+
appliedCount: 0,
|
|
70
|
+
failedCount: 0,
|
|
71
|
+
errorMessage: match.rejected.message,
|
|
72
|
+
});
|
|
73
|
+
log.warn(
|
|
74
|
+
{ accountConfigId, organizeJobId, reason: match.rejected.reason },
|
|
75
|
+
"Organize back-apply refused the job's rule; failing the job without a retry",
|
|
76
|
+
);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { messageIds, semanticUnavailable } = match;
|
|
49
81
|
const { applied, failed } = await applyOrganize(
|
|
50
82
|
{
|
|
51
83
|
client,
|