@remit/backend 0.0.143 → 0.0.145
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/dist/dev-server/content-handler.d.ts +38 -0
- package/dist/dev-server/content-handler.js +46 -0
- package/dist/src/api.d.ts +5 -0
- package/dist/src/api.js +73 -0
- package/dist/src/auth.d.ts +24 -0
- package/dist/src/auth.js +81 -0
- package/dist/src/config/api-config.d.ts +20 -0
- package/dist/src/config/api-config.js +1 -0
- package/dist/src/config/from-env.d.ts +2 -0
- package/dist/src/config/from-env.js +65 -0
- package/dist/src/config/msoauth-secrets-manager.d.ts +2 -0
- package/dist/src/config/msoauth-secrets-manager.js +28 -0
- package/dist/src/config/msoauth.d.ts +19 -0
- package/dist/src/config/msoauth.js +6 -0
- package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
- package/dist/src/derive/autoMoved.js +49 -0
- package/dist/src/derive/contentSignature.d.ts +40 -0
- package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +23 -77
- package/dist/src/derive/contentUrl.d.ts +33 -0
- package/dist/src/derive/contentUrl.js +27 -0
- package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
- package/dist/src/derive/deriveMuted.js +7 -0
- package/dist/src/derive/enrichThreadRows.d.ts +118 -0
- package/dist/src/derive/enrichThreadRows.js +151 -0
- package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
- package/dist/src/derive/filterThreadCriteria.js +24 -0
- package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
- package/dist/src/derive/pendingMoveCounts.js +53 -0
- package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
- package/dist/src/derive/senderTrust.js +19 -0
- package/dist/src/error.d.ts +21 -0
- package/dist/src/error.js +85 -0
- package/dist/src/handlers/account-guards.d.ts +36 -0
- package/dist/src/handlers/account-guards.js +107 -0
- package/dist/src/handlers/account-oauth.d.ts +15 -0
- package/dist/src/handlers/account-oauth.js +334 -0
- package/dist/src/handlers/account-overrides.d.ts +53 -0
- package/dist/src/handlers/account-overrides.js +233 -0
- package/dist/src/handlers/account-signature.d.ts +28 -0
- package/dist/src/handlers/account-signature.js +85 -0
- package/dist/src/handlers/account.d.ts +7 -0
- package/dist/src/handlers/account.js +337 -0
- package/dist/src/handlers/address.d.ts +35 -0
- package/dist/src/handlers/address.js +133 -0
- package/dist/src/handlers/calendar-event.d.ts +70 -0
- package/dist/src/handlers/calendar-event.js +417 -0
- package/dist/src/handlers/calendar-feed.d.ts +47 -0
- package/dist/src/handlers/calendar-feed.js +184 -0
- package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
- package/dist/src/handlers/calendar-suggestion.js +204 -0
- package/dist/src/handlers/calendar.d.ts +116 -0
- package/dist/src/handlers/calendar.js +323 -0
- package/dist/src/handlers/config.d.ts +43 -0
- package/dist/src/handlers/config.js +164 -0
- package/dist/src/handlers/ensure-account-config.d.ts +9 -0
- package/dist/src/handlers/ensure-account-config.js +27 -0
- package/dist/src/handlers/filter.d.ts +3 -0
- package/dist/src/handlers/filter.js +88 -0
- package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
- package/dist/src/handlers/folder-role-appointments.js +168 -0
- package/dist/src/handlers/folder-role-labels.d.ts +26 -0
- package/dist/src/handlers/folder-role-labels.js +40 -0
- package/dist/src/handlers/folder-role.d.ts +9 -0
- package/dist/src/handlers/folder-role.js +37 -0
- package/dist/src/handlers/index.d.ts +88 -0
- package/dist/src/handlers/index.js +62 -0
- package/dist/src/handlers/label.d.ts +43 -0
- package/dist/src/handlers/label.js +107 -0
- package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
- package/dist/src/handlers/mailbox-bindings.js +61 -0
- package/dist/src/handlers/mailbox.d.ts +23 -0
- package/dist/src/handlers/mailbox.js +211 -0
- package/dist/src/handlers/me.d.ts +2 -0
- package/dist/src/handlers/me.js +117 -0
- package/dist/src/handlers/message.d.ts +82 -0
- package/dist/src/handlers/message.js +595 -0
- package/dist/src/handlers/organize.d.ts +10 -0
- package/dist/src/handlers/organize.js +103 -0
- package/dist/src/handlers/outbox-attachment.d.ts +25 -0
- package/dist/src/handlers/outbox-attachment.js +77 -0
- package/dist/src/handlers/outbox.d.ts +4 -0
- package/dist/src/handlers/outbox.js +136 -0
- package/dist/src/handlers/sync-progress.d.ts +46 -0
- package/dist/src/handlers/sync-progress.js +63 -0
- package/dist/src/handlers/sync.d.ts +28 -0
- package/dist/src/handlers/sync.js +73 -0
- package/dist/src/handlers/system-update.d.ts +2 -0
- package/dist/src/handlers/system-update.js +159 -0
- package/dist/src/handlers/thread.d.ts +148 -0
- package/dist/src/handlers/thread.js +228 -0
- package/dist/src/handlers/unified-threads.d.ts +247 -0
- package/dist/src/handlers/unified-threads.js +406 -0
- package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
- package/dist/src/handlers/vip-suggestions.js +16 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +53 -0
- package/{src/json.ts → dist/src/json.d.ts} +1 -4
- package/dist/src/json.js +10 -0
- package/dist/src/jwt-auth.d.ts +15 -0
- package/dist/src/jwt-auth.js +45 -0
- package/dist/src/lambda.d.ts +13 -0
- package/dist/src/lambda.js +33 -0
- package/dist/src/request.d.ts +9 -0
- package/dist/src/request.js +17 -0
- package/dist/src/response.d.ts +44 -0
- package/dist/src/response.js +80 -0
- package/dist/src/serve.d.ts +31 -0
- package/dist/src/serve.js +117 -0
- package/dist/src/service/account-deletion.d.ts +20 -0
- package/dist/src/service/account-deletion.js +33 -0
- package/dist/src/service/compose-sqlite.d.ts +3 -0
- package/dist/src/service/compose-sqlite.js +13 -0
- package/dist/src/service/create-remit-client.d.ts +109 -0
- package/dist/src/service/create-remit-client.js +188 -0
- package/dist/src/service/data-client.d.ts +12 -0
- package/dist/src/service/data-client.js +42 -0
- package/dist/src/service/export-identity.d.ts +4 -0
- package/dist/src/service/export-identity.js +33 -0
- package/dist/src/service/fire-and-forget.d.ts +54 -0
- package/dist/src/service/fire-and-forget.js +37 -0
- package/dist/src/service/organize.d.ts +181 -0
- package/dist/src/service/organize.js +367 -0
- package/dist/src/service/queues.d.ts +12 -0
- package/dist/src/service/queues.js +1 -0
- package/dist/src/service/refusal.d.ts +4 -0
- package/dist/src/service/refusal.js +20 -0
- package/dist/src/service/sender-category.d.ts +51 -0
- package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
- package/dist/src/service/shared-deps.d.ts +6 -0
- package/dist/src/service/shared-deps.js +24 -0
- package/dist/src/service/trigger-sync.d.ts +57 -0
- package/dist/src/service/trigger-sync.js +31 -0
- package/dist/src/types.d.ts +72 -0
- package/dist/src/types.js +1 -0
- package/package.json +125 -98
- package/.env.test +0 -4
- package/dev-server/content-auth.test.ts +0 -113
- package/dev-server/content-auth.ts +0 -54
- package/dev-server/content-handler.test.ts +0 -105
- package/dev-server/content-handler.ts +0 -79
- package/dev-server/content-path.test.ts +0 -37
- package/dev-server/content-path.ts +0 -27
- package/dev-server/cors.test.ts +0 -48
- package/dev-server/cors.ts +0 -25
- package/dev-server/keep-alive.test.ts +0 -24
- package/dev-server/lambda-helpers.ts +0 -69
- package/dev-server/log-output.test.ts +0 -173
- package/dev-server/oauth-callback-route.test.ts +0 -66
- package/dev-server/relational-health.test.ts +0 -83
- package/dev-server/relational-health.ts +0 -39
- package/dev-server/server.ts +0 -384
- package/dev-server/sync-age.test.ts +0 -91
- package/dev-server/sync-age.ts +0 -55
- package/dev-server/upload-handler.ts +0 -132
- package/dev-server/upload-route.test.ts +0 -196
- package/scripts/backfill-classification.ts +0 -91
- package/scripts/backfill-list-id.ts +0 -83
- package/scripts/config-save.ts +0 -128
- package/src/auth.test.ts +0 -12
- package/src/auth.ts +0 -92
- package/src/config/msoauth.test.ts +0 -48
- package/src/config/msoauth.ts +0 -48
- package/src/data-backend.test.ts +0 -23
- package/src/data-backend.ts +0 -14
- package/src/derive/autoMoved.test.ts +0 -104
- package/src/derive/contentSignature.test.ts +0 -161
- package/src/derive/contentUrl.test.ts +0 -172
- package/src/derive/contentUrl.ts +0 -86
- package/src/derive/deriveMuted.test.ts +0 -34
- package/src/derive/enrichThreadRows.test.ts +0 -360
- package/src/derive/enrichThreadRows.ts +0 -234
- package/src/derive/filterThreadCriteria.test.ts +0 -126
- package/src/derive/filterThreadCriteria.ts +0 -51
- package/src/derive/senderTrust.test.ts +0 -67
- package/src/error.test.ts +0 -201
- package/src/error.ts +0 -123
- package/src/handlers/account-guards.test.ts +0 -67
- package/src/handlers/account-guards.ts +0 -169
- package/src/handlers/account-oauth-callback.test.ts +0 -155
- package/src/handlers/account-oauth-consent.test.ts +0 -336
- package/src/handlers/account-oauth-start.test.ts +0 -191
- package/src/handlers/account-oauth.test.ts +0 -439
- package/src/handlers/account-oauth.ts +0 -635
- package/src/handlers/account-overrides.test.ts +0 -88
- package/src/handlers/account-overrides.ts +0 -323
- package/src/handlers/account-ownership.ts +0 -25
- package/src/handlers/account-signature.ts +0 -117
- package/src/handlers/account-synced-services.test.ts +0 -245
- package/src/handlers/account.ts +0 -552
- package/src/handlers/address.test.ts +0 -381
- package/src/handlers/address.ts +0 -200
- package/src/handlers/calendar-event.test.ts +0 -685
- package/src/handlers/calendar-event.ts +0 -738
- package/src/handlers/calendar-feed-validators.test.ts +0 -228
- package/src/handlers/calendar-feed.test.ts +0 -439
- package/src/handlers/calendar-feed.ts +0 -291
- package/src/handlers/calendar-sqlite-fixture.ts +0 -36
- package/src/handlers/calendar-suggestion.test.ts +0 -1269
- package/src/handlers/calendar-suggestion.ts +0 -425
- package/src/handlers/calendar.test.ts +0 -1080
- package/src/handlers/calendar.ts +0 -517
- package/src/handlers/config.test.ts +0 -245
- package/src/handlers/config.ts +0 -332
- package/src/handlers/ensure-account-config.ts +0 -30
- package/src/handlers/filter.test.ts +0 -644
- package/src/handlers/filter.ts +0 -518
- package/src/handlers/folder-role-appointments.test.ts +0 -438
- package/src/handlers/folder-role-appointments.ts +0 -249
- package/src/handlers/folder-role-labels.ts +0 -63
- package/src/handlers/folder-role.test.ts +0 -209
- package/src/handlers/folder-role.ts +0 -80
- package/src/handlers/index.ts +0 -83
- package/src/handlers/label.test.ts +0 -112
- package/src/handlers/label.ts +0 -210
- package/src/handlers/mailbox-bindings.ts +0 -111
- package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
- package/src/handlers/mailbox-settled-target.test.ts +0 -251
- package/src/handlers/mailbox.test.ts +0 -280
- package/src/handlers/mailbox.ts +0 -581
- package/src/handlers/me.test.ts +0 -81
- package/src/handlers/me.ts +0 -203
- package/src/handlers/message-read-backfill.test.ts +0 -103
- package/src/handlers/message.test.ts +0 -131
- package/src/handlers/message.ts +0 -1131
- package/src/handlers/organize.test.ts +0 -277
- package/src/handlers/organize.ts +0 -197
- package/src/handlers/outbox-attachment.test.ts +0 -765
- package/src/handlers/outbox-attachment.ts +0 -126
- package/src/handlers/outbox-list.test.ts +0 -179
- package/src/handlers/outbox.test.ts +0 -772
- package/src/handlers/outbox.ts +0 -258
- package/src/handlers/search.test.ts +0 -161
- package/src/handlers/search.ts +0 -105
- package/src/handlers/sync-progress.test.ts +0 -205
- package/src/handlers/sync-progress.ts +0 -99
- package/src/handlers/sync.test.ts +0 -168
- package/src/handlers/sync.ts +0 -118
- package/src/handlers/system-update.test.ts +0 -497
- package/src/handlers/system-update.ts +0 -230
- package/src/handlers/thread.test.ts +0 -396
- package/src/handlers/thread.ts +0 -427
- package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
- package/src/handlers/unified-threads.test.ts +0 -785
- package/src/handlers/unified-threads.ts +0 -669
- package/src/index.test.ts +0 -83
- package/src/index.ts +0 -228
- package/src/jwt-auth.test.ts +0 -152
- package/src/jwt-auth.ts +0 -132
- package/src/log-redaction.test.ts +0 -144
- package/src/openapi-spec.test.ts +0 -118
- package/src/request-context.test.ts +0 -61
- package/src/request-context.ts +0 -33
- package/src/request-pipeline.test.ts +0 -88
- package/src/request.ts +0 -10
- package/src/response.test.ts +0 -130
- package/src/response.ts +0 -144
- package/src/service/compose-sqlite.ts +0 -98
- package/src/service/config-import.ts +0 -39
- package/src/service/create-remit-client.ts +0 -522
- package/src/service/data-client.test.ts +0 -97
- package/src/service/data-client.ts +0 -58
- package/src/service/export-identity.ts +0 -39
- package/src/service/filter.ts +0 -40
- package/src/service/fire-and-forget.test.ts +0 -126
- package/src/service/fire-and-forget.ts +0 -79
- package/src/service/localhost-env-config.test.ts +0 -36
- package/src/service/organize-queue.ts +0 -98
- package/src/service/organize.test.ts +0 -1451
- package/src/service/organize.ts +0 -1025
- package/src/service/read-path-filter-config.test.ts +0 -243
- package/src/service/semantic-capability.test.ts +0 -78
- package/src/service/semantic-capability.ts +0 -95
- package/src/service/sender-category.test.ts +0 -235
- package/src/service/sqs.ts +0 -4
- package/src/service/trigger-sync.test.ts +0 -205
- package/src/service/trigger-sync.ts +0 -100
- package/src/stdout-capture-fixture.ts +0 -40
- package/src/types.ts +0 -271
- package/tsconfig.json +0 -7
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import type {
|
|
5
|
-
SystemUpdateResponse,
|
|
6
|
-
SystemUpdateRun,
|
|
7
|
-
} from "@remit/api-openapi-types";
|
|
8
|
-
import { ConflictError, HTTPError } from "@remit/data-ports/errors";
|
|
9
|
-
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
|
|
10
|
-
import type { Context } from "openapi-backend";
|
|
11
|
-
import { getSubFromEvent } from "../auth.js";
|
|
12
|
-
import { defaultErrorCode } from "../error.js";
|
|
13
|
-
import type { OperationHandler, SystemOperationIds } from "../types.js";
|
|
14
|
-
|
|
15
|
-
class TargetVersionMismatchError extends HTTPError {
|
|
16
|
-
name = "TargetVersionMismatchError";
|
|
17
|
-
statusCode = 422;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const DEFAULT_CONTROL_DIR = "/data/control";
|
|
21
|
-
|
|
22
|
-
const controlDir = (): string =>
|
|
23
|
-
process.env.REMIT_UPDATE_CONTROL_DIR ?? DEFAULT_CONTROL_DIR;
|
|
24
|
-
|
|
25
|
-
const manifestUrl = (): string | undefined =>
|
|
26
|
-
process.env.REMIT_UPDATE_MANIFEST_URL;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* The running version is a fact the updater owns: it reads the tag from `.env`
|
|
30
|
-
* and writes it into `state.json`, which is the only honest source across an
|
|
31
|
-
* update that rewrites that tag underneath a long-lived backend process. When no
|
|
32
|
-
* state file exists yet the backend has nothing authoritative to report, so it
|
|
33
|
-
* reports the version as unknown rather than fabricating one from its own
|
|
34
|
-
* process environment — a value captured at container start that drifts from the
|
|
35
|
-
* real running tag the moment an update lands. The first updater check writes a
|
|
36
|
-
* real `currentVersion` within seconds of the stack coming up.
|
|
37
|
-
*/
|
|
38
|
-
const UNKNOWN_VERSION = "unknown";
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* A refusal in the shape `formatResponse` unwraps: the status, and the body as
|
|
42
|
-
* an object it serializes. Handing it an already-serialized `body` string
|
|
43
|
-
* instead publishes the whole envelope — `{"statusCode":…,"headers":…}` with
|
|
44
|
-
* the real body escaped inside it — which is not an `ApiError` at all.
|
|
45
|
-
*/
|
|
46
|
-
const refuse = (statusCode: number, message: string): APIGatewayProxyResult =>
|
|
47
|
-
({
|
|
48
|
-
statusCode,
|
|
49
|
-
body: { code: defaultErrorCode(statusCode), message },
|
|
50
|
-
}) as unknown as APIGatewayProxyResult;
|
|
51
|
-
|
|
52
|
-
const notFound = (): APIGatewayProxyResult => refuse(404, "Not found");
|
|
53
|
-
|
|
54
|
-
const unauthorized = (): APIGatewayProxyResult => refuse(401, "Unauthorized");
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* The self-update seam is off unless a manifest URL is configured (RFC 037 D8),
|
|
58
|
-
* which the hosted deployment leaves unset. Both endpoints answer 404 in that
|
|
59
|
-
* case. Where it is on, any authenticated caller may use it — the account list
|
|
60
|
-
* is the trust boundary of a self-hosted instance.
|
|
61
|
-
*/
|
|
62
|
-
const guardManifestConfigured = (): APIGatewayProxyResult | null =>
|
|
63
|
-
manifestUrl() ? null : notFound();
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Read `state.json` off the control volume. A missing file is an instance that
|
|
67
|
-
* has never checked or updated (RFC 037 D8), not an error, so it reads as
|
|
68
|
-
* `null`; the caller renders the disabled/empty resource. Any other read or
|
|
69
|
-
* parse failure propagates.
|
|
70
|
-
*/
|
|
71
|
-
const readState = (): SystemUpdateResponse | null => {
|
|
72
|
-
let raw: string;
|
|
73
|
-
try {
|
|
74
|
-
raw = readFileSync(join(controlDir(), "state.json"), "utf8");
|
|
75
|
-
} catch (error) {
|
|
76
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
return JSON.parse(raw) as SystemUpdateResponse;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const emptyResource = (): SystemUpdateResponse => ({
|
|
83
|
-
currentVersion: UNKNOWN_VERSION,
|
|
84
|
-
check: { status: "disabled" },
|
|
85
|
-
run: null,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
const isRunInFlight = (run: SystemUpdateRun | null | undefined): boolean =>
|
|
89
|
-
run != null && run.outcome === null;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* The version the caller may install is the one the last check reported. It
|
|
93
|
-
* exists only on a successful check; a disabled or failed check offers nothing
|
|
94
|
-
* to match, so any target is a mismatch.
|
|
95
|
-
*/
|
|
96
|
-
const checkedVersion = (
|
|
97
|
-
state: SystemUpdateResponse | null,
|
|
98
|
-
): string | undefined =>
|
|
99
|
-
state?.check.status === "ok" ? state.check.latestVersion : undefined;
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Write `request.json` atomically with exactly the four fields the seam accepts
|
|
103
|
-
* (#133). The updater resolves the registry and every image reference from the
|
|
104
|
-
* manifest it fetches itself; a request naming any of those is rejected whole,
|
|
105
|
-
* so the backend never writes one. The temp file sits in the same directory as
|
|
106
|
-
* the target for the rename to be atomic on one filesystem.
|
|
107
|
-
*/
|
|
108
|
-
const writeRequest = (request: {
|
|
109
|
-
runId: string;
|
|
110
|
-
targetVersion: string;
|
|
111
|
-
requestedAt: string;
|
|
112
|
-
requestedBy: string;
|
|
113
|
-
}): void => {
|
|
114
|
-
const dir = controlDir();
|
|
115
|
-
const tmp = join(dir, `.request.json.${request.runId}.tmp`);
|
|
116
|
-
writeFileSync(tmp, JSON.stringify(request), { mode: 0o644 });
|
|
117
|
-
renameSync(tmp, join(dir, "request.json"));
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Record a check request on the control seam (#599). The updater consumes it on
|
|
122
|
-
* its watch loop and runs a manifest check immediately, so a press of check in
|
|
123
|
-
* the panel — not just the updater's own cadence — moves `lastCheckedAt`. Like
|
|
124
|
-
* request.json it is written atomically and carries no authority: its presence
|
|
125
|
-
* is the whole message, so it is empty.
|
|
126
|
-
*/
|
|
127
|
-
const writeCheckRequest = (): void => {
|
|
128
|
-
const dir = controlDir();
|
|
129
|
-
const tmp = join(dir, `.check-request.json.tmp`);
|
|
130
|
-
writeFileSync(tmp, JSON.stringify({}), { mode: 0o644 });
|
|
131
|
-
renameSync(tmp, join(dir, "check-request.json"));
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* The resource returned by the POST. The updater has not yet written the
|
|
136
|
-
* authoritative run — it polls the seam — so this bootstraps the run block with
|
|
137
|
-
* the id just requested and the first phase, giving the client a `runId` to poll
|
|
138
|
-
* for (RFC 037 D9). The updater's own `state.json` supersedes it on the next
|
|
139
|
-
* read.
|
|
140
|
-
*/
|
|
141
|
-
const requestedResource = (
|
|
142
|
-
state: SystemUpdateResponse | null,
|
|
143
|
-
runId: string,
|
|
144
|
-
targetVersion: string,
|
|
145
|
-
requestedAt: string,
|
|
146
|
-
): SystemUpdateResponse => {
|
|
147
|
-
const from = state?.currentVersion ?? UNKNOWN_VERSION;
|
|
148
|
-
return {
|
|
149
|
-
currentVersion: from,
|
|
150
|
-
check: state?.check ?? { status: "disabled" },
|
|
151
|
-
run: {
|
|
152
|
-
runId,
|
|
153
|
-
fromVersion: from,
|
|
154
|
-
targetVersion,
|
|
155
|
-
phase: "checking",
|
|
156
|
-
outcome: null,
|
|
157
|
-
startedAt: requestedAt,
|
|
158
|
-
updatedAt: requestedAt,
|
|
159
|
-
message: "The update has been requested.",
|
|
160
|
-
logCommand: "remit logs updater",
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
export const SystemOperations: Record<
|
|
166
|
-
SystemOperationIds,
|
|
167
|
-
OperationHandler<SystemOperationIds>
|
|
168
|
-
> = {
|
|
169
|
-
SystemOperations_getSystemUpdate: async (
|
|
170
|
-
context: Context,
|
|
171
|
-
...args: unknown[]
|
|
172
|
-
): Promise<SystemUpdateResponse | APIGatewayProxyResult> => {
|
|
173
|
-
const offSurface = guardManifestConfigured();
|
|
174
|
-
if (offSurface) return offSurface;
|
|
175
|
-
|
|
176
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
177
|
-
if (!getSubFromEvent(event)) return unauthorized();
|
|
178
|
-
|
|
179
|
-
// A refresh asks for a fresh answer, and only the updater can fetch one, so
|
|
180
|
-
// the request goes on the control seam for its watch loop to pick up (#599).
|
|
181
|
-
// The answer is the stored state as it stands, `lastCheckedAt` included: the
|
|
182
|
-
// caller holds its own press and watches that timestamp move, so the
|
|
183
|
-
// response never has to claim a verdict the updater has not reached.
|
|
184
|
-
const { refresh } = context.request.query as { refresh?: boolean };
|
|
185
|
-
if (refresh === true) writeCheckRequest();
|
|
186
|
-
|
|
187
|
-
return readState() ?? emptyResource();
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
SystemOperations_applySystemUpdate: async (
|
|
191
|
-
context: Context,
|
|
192
|
-
...args: unknown[]
|
|
193
|
-
): Promise<SystemUpdateResponse | APIGatewayProxyResult> => {
|
|
194
|
-
const offSurface = guardManifestConfigured();
|
|
195
|
-
if (offSurface) return offSurface;
|
|
196
|
-
|
|
197
|
-
const event = args[0] as APIGatewayProxyEvent;
|
|
198
|
-
const sub = getSubFromEvent(event);
|
|
199
|
-
if (!sub) return unauthorized();
|
|
200
|
-
|
|
201
|
-
const { targetVersion } = context.request.requestBody as {
|
|
202
|
-
targetVersion: string;
|
|
203
|
-
};
|
|
204
|
-
const state = readState();
|
|
205
|
-
|
|
206
|
-
if (isRunInFlight(state?.run)) {
|
|
207
|
-
throw new ConflictError("An update is already in progress.");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (targetVersion !== checkedVersion(state)) {
|
|
211
|
-
throw new TargetVersionMismatchError(
|
|
212
|
-
"targetVersion does not match the latest checked release.",
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const runId = randomUUID();
|
|
217
|
-
const requestedAt = new Date().toISOString();
|
|
218
|
-
writeRequest({
|
|
219
|
-
runId,
|
|
220
|
-
targetVersion,
|
|
221
|
-
requestedAt,
|
|
222
|
-
requestedBy: sub,
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
return {
|
|
226
|
-
statusCode: 202,
|
|
227
|
-
body: requestedResource(state, runId, targetVersion, requestedAt),
|
|
228
|
-
} as unknown as APIGatewayProxyResult;
|
|
229
|
-
},
|
|
230
|
-
};
|
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type { ThreadMessageItem } from "@remit/data-ports";
|
|
4
|
-
import { MessageCategory, SenderTrust, StarColor } from "@remit/domain-enums";
|
|
5
|
-
import {
|
|
6
|
-
buildListThreadMessagesOptions,
|
|
7
|
-
buildListThreadsOptions,
|
|
8
|
-
buildSearchThreadsOptions,
|
|
9
|
-
dedupeThreadMessages,
|
|
10
|
-
executeThreadSearch,
|
|
11
|
-
type ThreadSearchClient,
|
|
12
|
-
} from "./thread.js";
|
|
13
|
-
|
|
14
|
-
const ACCOUNT = "cfg-1";
|
|
15
|
-
const MAILBOX = "mbx-inbox";
|
|
16
|
-
|
|
17
|
-
type Row = {
|
|
18
|
-
threadMessageId: string;
|
|
19
|
-
messageIdHeader?: string;
|
|
20
|
-
createdAt: number;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const row = (
|
|
24
|
-
threadMessageId: string,
|
|
25
|
-
messageIdHeader: string | undefined,
|
|
26
|
-
createdAt: number,
|
|
27
|
-
): Row => ({ threadMessageId, messageIdHeader, createdAt });
|
|
28
|
-
|
|
29
|
-
describe("buildListThreadMessagesOptions", () => {
|
|
30
|
-
it("defaults to oldest-first and hides soft-deleted messages", () => {
|
|
31
|
-
assert.deepEqual(buildListThreadMessagesOptions({}), {
|
|
32
|
-
order: "asc",
|
|
33
|
-
excludeDeleted: true,
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("honours an explicit order", () => {
|
|
38
|
-
assert.equal(
|
|
39
|
-
buildListThreadMessagesOptions({ order: "desc" }).order,
|
|
40
|
-
"desc",
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("carries no mailbox filter, so sent messages stay in the conversation", () => {
|
|
45
|
-
assert.ok(!("mailboxId" in buildListThreadMessagesOptions({})));
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe("dedupeThreadMessages", () => {
|
|
50
|
-
it("keeps a thread whose messages are all distinct", () => {
|
|
51
|
-
const rows = [
|
|
52
|
-
row("tm-1", "<a@example.test>", 10),
|
|
53
|
-
row("tm-2", "<b@example.test>", 20),
|
|
54
|
-
];
|
|
55
|
-
assert.deepEqual(dedupeThreadMessages(rows), rows);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("collapses a copied message to the original", () => {
|
|
59
|
-
const original = row("tm-1", "<a@example.test>", 10);
|
|
60
|
-
const copy = row("tm-2", "<a@example.test>", 50);
|
|
61
|
-
assert.deepEqual(dedupeThreadMessages([original, copy]), [original]);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("keeps the same row whichever order the rows arrive in", () => {
|
|
65
|
-
const original = row("tm-1", "<a@example.test>", 10);
|
|
66
|
-
const copy = row("tm-2", "<a@example.test>", 50);
|
|
67
|
-
assert.deepEqual(dedupeThreadMessages([copy, original]), [original]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("breaks a createdAt tie on threadMessageId", () => {
|
|
71
|
-
const first = row("tm-a", "<a@example.test>", 10);
|
|
72
|
-
const second = row("tm-b", "<a@example.test>", 10);
|
|
73
|
-
assert.deepEqual(dedupeThreadMessages([second, first]), [first]);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("treats headers that differ only by surrounding space as one message", () => {
|
|
77
|
-
const original = row("tm-1", "<a@example.test>", 10);
|
|
78
|
-
const copy = row("tm-2", " <a@example.test> ", 50);
|
|
79
|
-
assert.deepEqual(dedupeThreadMessages([original, copy]), [original]);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("never merges rows without a usable header", () => {
|
|
83
|
-
const rows = [
|
|
84
|
-
row("tm-1", undefined, 10),
|
|
85
|
-
row("tm-2", undefined, 20),
|
|
86
|
-
row("tm-3", "<>", 30),
|
|
87
|
-
row("tm-4", "", 40),
|
|
88
|
-
];
|
|
89
|
-
assert.deepEqual(dedupeThreadMessages(rows), rows);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("leaves an empty thread empty", () => {
|
|
93
|
-
assert.deepEqual(dedupeThreadMessages([]), []);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// #304: `category` is a column on the thread_message row, so it is a SQL
|
|
98
|
-
// predicate the port applies inside its window — not a criterion resolved by
|
|
99
|
-
// enriching whatever the window happened to return. These assertions are on the
|
|
100
|
-
// routing, because that is what a later refactor can silently undo: the SQL
|
|
101
|
-
// clause goes dead and the filter falls back to a window-sized filter with no
|
|
102
|
-
// test failing.
|
|
103
|
-
describe("executeThreadSearch", () => {
|
|
104
|
-
type Category = ThreadMessageItem["category"];
|
|
105
|
-
|
|
106
|
-
const threadRow = (
|
|
107
|
-
threadMessageId: string,
|
|
108
|
-
category: Category,
|
|
109
|
-
): ThreadMessageItem => ({
|
|
110
|
-
threadMessageId,
|
|
111
|
-
threadId: `t-${threadMessageId}`,
|
|
112
|
-
messageId: `m-${threadMessageId}`,
|
|
113
|
-
accountConfigId: ACCOUNT,
|
|
114
|
-
mailboxId: MAILBOX,
|
|
115
|
-
uid: 1,
|
|
116
|
-
referenceOrder: 0,
|
|
117
|
-
internalDate: 0,
|
|
118
|
-
sentDate: 0,
|
|
119
|
-
isRead: false,
|
|
120
|
-
hasAttachment: false,
|
|
121
|
-
star: StarColor.None,
|
|
122
|
-
hasStars: false,
|
|
123
|
-
isDeleted: false,
|
|
124
|
-
category,
|
|
125
|
-
createdAt: 0,
|
|
126
|
-
updatedAt: 0,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
type RecordedCall = {
|
|
130
|
-
search: { category?: Category[] };
|
|
131
|
-
continuationToken?: string;
|
|
132
|
-
limit?: number;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const fakeClient = (rows: ThreadMessageItem[]) => {
|
|
136
|
-
const windowCalls: RecordedCall[] = [];
|
|
137
|
-
const countCalls: RecordedCall[] = [];
|
|
138
|
-
|
|
139
|
-
// The fake applies the category predicate itself, the way a port does, so
|
|
140
|
-
// a request that never reaches `search` cannot answer correctly by
|
|
141
|
-
// accident. The window also honours `limit`, so a handler deriving a count
|
|
142
|
-
// from the rows it got back cannot pass for one that asked the port.
|
|
143
|
-
const matching = (categories?: Category[]) =>
|
|
144
|
-
categories?.length
|
|
145
|
-
? rows.filter((row) => categories.includes(row.category))
|
|
146
|
-
: rows;
|
|
147
|
-
|
|
148
|
-
const client: ThreadSearchClient = {
|
|
149
|
-
threadMessage: {
|
|
150
|
-
async searchByMailboxWindow(_account, _mailbox, search, options) {
|
|
151
|
-
windowCalls.push({
|
|
152
|
-
search,
|
|
153
|
-
continuationToken: options?.continuationToken,
|
|
154
|
-
limit: options?.limit,
|
|
155
|
-
});
|
|
156
|
-
return {
|
|
157
|
-
items: matching(search.category).slice(0, options?.limit),
|
|
158
|
-
continuationToken: undefined,
|
|
159
|
-
};
|
|
160
|
-
},
|
|
161
|
-
async countByMailbox(_account, _mailbox, search) {
|
|
162
|
-
countCalls.push({ search });
|
|
163
|
-
return matching(search.category).length;
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
message: { get: async () => [] },
|
|
167
|
-
address: { getAddress: async () => [] },
|
|
168
|
-
messageLabel: { listByMessageIds: async () => [] },
|
|
169
|
-
label: { listByAccountConfig: async () => [] },
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
return { client, windowCalls, countCalls };
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const ACCOUNT_ROWS = [
|
|
176
|
-
threadRow("tm-1", MessageCategory.personal),
|
|
177
|
-
threadRow("tm-2", MessageCategory.marketing),
|
|
178
|
-
threadRow("tm-3", MessageCategory.uncategorized),
|
|
179
|
-
];
|
|
180
|
-
|
|
181
|
-
it("passes category to the port's search, not to the off-row filter", async () => {
|
|
182
|
-
const { client, windowCalls } = fakeClient(ACCOUNT_ROWS);
|
|
183
|
-
|
|
184
|
-
const response = await executeThreadSearch(client, ACCOUNT, MAILBOX, {
|
|
185
|
-
category: [MessageCategory.personal],
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
assert.deepEqual(windowCalls.length, 1);
|
|
189
|
-
assert.deepEqual(windowCalls[0].search.category, [
|
|
190
|
-
MessageCategory.personal,
|
|
191
|
-
]);
|
|
192
|
-
assert.deepEqual(
|
|
193
|
-
response.items?.map((item) => item.threadMessageId),
|
|
194
|
-
["tm-1"],
|
|
195
|
-
);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("takes the count-only path for a category-only query", async () => {
|
|
199
|
-
const { client, windowCalls, countCalls } = fakeClient(ACCOUNT_ROWS);
|
|
200
|
-
|
|
201
|
-
const response = await executeThreadSearch(client, ACCOUNT, MAILBOX, {
|
|
202
|
-
category: [MessageCategory.personal, MessageCategory.marketing],
|
|
203
|
-
count: true,
|
|
204
|
-
results: false,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
assert.equal(windowCalls.length, 0, "no window read in count-only mode");
|
|
208
|
-
assert.equal(countCalls.length, 1);
|
|
209
|
-
assert.deepEqual(countCalls[0].search.category, [
|
|
210
|
-
MessageCategory.personal,
|
|
211
|
-
MessageCategory.marketing,
|
|
212
|
-
]);
|
|
213
|
-
assert.equal(response.count, 2);
|
|
214
|
-
assert.equal(response.items, undefined);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// The one request shape that still takes the off-row branch. `category` must
|
|
218
|
-
// reach `search` anyway, so the window is category-filtered before the
|
|
219
|
-
// enrichment the off-row criterion needs.
|
|
220
|
-
it("keeps category in search when an off-row criterion is also set", async () => {
|
|
221
|
-
const { client, windowCalls } = fakeClient(ACCOUNT_ROWS);
|
|
222
|
-
|
|
223
|
-
const response = await executeThreadSearch(client, ACCOUNT, MAILBOX, {
|
|
224
|
-
category: [MessageCategory.personal],
|
|
225
|
-
senderTrust: [SenderTrust.Unknown],
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
assert.equal(windowCalls.length, 1);
|
|
229
|
-
assert.deepEqual(windowCalls[0].search.category, [
|
|
230
|
-
MessageCategory.personal,
|
|
231
|
-
]);
|
|
232
|
-
assert.deepEqual(
|
|
233
|
-
response.items?.map((item) => item.threadMessageId),
|
|
234
|
-
["tm-1"],
|
|
235
|
-
);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it("serves category from the row, so it survives an absent message row", async () => {
|
|
239
|
-
const { client } = fakeClient(ACCOUNT_ROWS);
|
|
240
|
-
|
|
241
|
-
const response = await executeThreadSearch(client, ACCOUNT, MAILBOX, {});
|
|
242
|
-
|
|
243
|
-
assert.deepEqual(
|
|
244
|
-
response.items?.map((item) => item.category),
|
|
245
|
-
[
|
|
246
|
-
MessageCategory.personal,
|
|
247
|
-
MessageCategory.marketing,
|
|
248
|
-
MessageCategory.uncategorized,
|
|
249
|
-
],
|
|
250
|
-
);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// #509: `limit` is a page size. A response that carries a page of rows and a
|
|
254
|
-
// count has to state how many messages match, not how many it handed back.
|
|
255
|
-
it("counts the whole match when rows are asked for as well", async () => {
|
|
256
|
-
const pageSize = 2;
|
|
257
|
-
const matches = 4753;
|
|
258
|
-
const windowCalls: number[] = [];
|
|
259
|
-
const client: ThreadSearchClient = {
|
|
260
|
-
threadMessage: {
|
|
261
|
-
async searchByMailboxWindow(_account, _mailbox, _search, options) {
|
|
262
|
-
windowCalls.push(options?.limit ?? 0);
|
|
263
|
-
return {
|
|
264
|
-
items: ACCOUNT_ROWS.slice(0, pageSize),
|
|
265
|
-
continuationToken: "next",
|
|
266
|
-
};
|
|
267
|
-
},
|
|
268
|
-
async countByMailbox() {
|
|
269
|
-
return matches;
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
message: { get: async () => [] },
|
|
273
|
-
address: { getAddress: async () => [] },
|
|
274
|
-
messageLabel: { listByMessageIds: async () => [] },
|
|
275
|
-
label: { listByAccountConfig: async () => [] },
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
const response = await executeThreadSearch(client, ACCOUNT, MAILBOX, {
|
|
279
|
-
count: true,
|
|
280
|
-
limit: pageSize,
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
assert.deepEqual(windowCalls, [pageSize]);
|
|
284
|
-
assert.equal(response.items?.length, pageSize);
|
|
285
|
-
assert.equal(response.count, matches);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// #305: `senderTrust` and `dkimMismatch` are resolved by enriching rows, so
|
|
289
|
-
// the only number the handler could put in `count` is this page's post-filter
|
|
290
|
-
// length — a page length wearing the name of a total. Absent instead.
|
|
291
|
-
it("omits count under senderTrust rather than counting the page", async () => {
|
|
292
|
-
const { client, countCalls } = fakeClient(ACCOUNT_ROWS);
|
|
293
|
-
|
|
294
|
-
const response = await executeThreadSearch(client, ACCOUNT, MAILBOX, {
|
|
295
|
-
senderTrust: [SenderTrust.Unknown],
|
|
296
|
-
count: true,
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
assert.equal(response.count, undefined);
|
|
300
|
-
assert.equal(countCalls.length, 0, "nothing to count off the row");
|
|
301
|
-
assert.equal(response.items?.length, ACCOUNT_ROWS.length);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it("omits count under dkimMismatch rather than counting the page", async () => {
|
|
305
|
-
const { client } = fakeClient(ACCOUNT_ROWS);
|
|
306
|
-
|
|
307
|
-
const response = await executeThreadSearch(client, ACCOUNT, MAILBOX, {
|
|
308
|
-
dkimMismatch: true,
|
|
309
|
-
count: true,
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
assert.equal(response.count, undefined);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
// The count it asked for cannot be answered and the rows it declined are not
|
|
316
|
-
// wanted, so there is nothing left to read.
|
|
317
|
-
it("reads nothing for a count-only request under an off-row criterion", async () => {
|
|
318
|
-
const { client, windowCalls, countCalls } = fakeClient(ACCOUNT_ROWS);
|
|
319
|
-
|
|
320
|
-
const response = await executeThreadSearch(client, ACCOUNT, MAILBOX, {
|
|
321
|
-
dkimMismatch: true,
|
|
322
|
-
count: true,
|
|
323
|
-
results: false,
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
assert.deepEqual(response, {});
|
|
327
|
-
assert.equal(windowCalls.length, 0);
|
|
328
|
-
assert.equal(countCalls.length, 0);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it("answers the same count whether or not rows were asked for", async () => {
|
|
332
|
-
const criteria = {
|
|
333
|
-
category: [MessageCategory.personal, MessageCategory.marketing],
|
|
334
|
-
count: true,
|
|
335
|
-
};
|
|
336
|
-
const withRows = await executeThreadSearch(
|
|
337
|
-
fakeClient(ACCOUNT_ROWS).client,
|
|
338
|
-
ACCOUNT,
|
|
339
|
-
MAILBOX,
|
|
340
|
-
criteria,
|
|
341
|
-
);
|
|
342
|
-
const countOnly = await executeThreadSearch(
|
|
343
|
-
fakeClient(ACCOUNT_ROWS).client,
|
|
344
|
-
ACCOUNT,
|
|
345
|
-
MAILBOX,
|
|
346
|
-
{ ...criteria, results: false },
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
assert.equal(withRows.count, 2);
|
|
350
|
-
assert.equal(countOnly.count, withRows.count);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// The page size bounds the rows; it has no say in how many messages match.
|
|
354
|
-
it("holds the count still while the page size moves", async () => {
|
|
355
|
-
const responses = [];
|
|
356
|
-
for (const limit of [1, 500]) {
|
|
357
|
-
const { client } = fakeClient(ACCOUNT_ROWS);
|
|
358
|
-
responses.push(
|
|
359
|
-
await executeThreadSearch(client, ACCOUNT, MAILBOX, {
|
|
360
|
-
count: true,
|
|
361
|
-
limit,
|
|
362
|
-
}),
|
|
363
|
-
);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
assert.deepEqual(
|
|
367
|
-
responses.map((response) => response.items?.length),
|
|
368
|
-
[1, ACCOUNT_ROWS.length],
|
|
369
|
-
"the page did move",
|
|
370
|
-
);
|
|
371
|
-
assert.deepEqual(
|
|
372
|
-
responses.map((response) => response.count),
|
|
373
|
-
[ACCOUNT_ROWS.length, ACCOUNT_ROWS.length],
|
|
374
|
-
);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// The cursor never reaches the count query, so the number does not shrink as
|
|
378
|
-
// the user pages: page three claims the same total page one did.
|
|
379
|
-
it("holds the count still while the caller pages", async () => {
|
|
380
|
-
const { client, windowCalls, countCalls } = fakeClient(ACCOUNT_ROWS);
|
|
381
|
-
|
|
382
|
-
const response = await executeThreadSearch(client, ACCOUNT, MAILBOX, {
|
|
383
|
-
count: true,
|
|
384
|
-
continuationToken: "page-three",
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
assert.equal(windowCalls[0].continuationToken, "page-three");
|
|
388
|
-
assert.equal(countCalls.length, 1);
|
|
389
|
-
assert.equal(response.count, ACCOUNT_ROWS.length);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it("projects category, so the DynamoDB port reads it with the row", () => {
|
|
393
|
-
assert.ok(buildSearchThreadsOptions({}).attributes.includes("category"));
|
|
394
|
-
assert.ok(buildListThreadsOptions({}).attributes.includes("category"));
|
|
395
|
-
});
|
|
396
|
-
});
|