@remit/data-ports 0.0.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/package.json +54 -0
- package/src/account-settings.ts +192 -0
- package/src/conformance/harness.ts +12 -0
- package/src/conformance/index.ts +2 -0
- package/src/conformance/label.ts +95 -0
- package/src/errors.ts +75 -0
- package/src/id.ts +105 -0
- package/src/index.ts +102 -0
- package/src/interfaces/account-config.ts +25 -0
- package/src/interfaces/account-export-request.ts +21 -0
- package/src/interfaces/account-setting.ts +14 -0
- package/src/interfaces/account.ts +40 -0
- package/src/interfaces/address.ts +82 -0
- package/src/interfaces/envelope.ts +32 -0
- package/src/interfaces/filter-anchor.ts +11 -0
- package/src/interfaces/filter.ts +41 -0
- package/src/interfaces/label.ts +21 -0
- package/src/interfaces/mailbox-lock.ts +31 -0
- package/src/interfaces/mailbox-special-use.ts +33 -0
- package/src/interfaces/mailbox.ts +59 -0
- package/src/interfaces/message-flag-push.ts +17 -0
- package/src/interfaces/message-flag.ts +15 -0
- package/src/interfaces/message-label.ts +11 -0
- package/src/interfaces/message-placement-move.ts +15 -0
- package/src/interfaces/message.ts +40 -0
- package/src/interfaces/organize-job-request.ts +19 -0
- package/src/interfaces/outbox-message.ts +51 -0
- package/src/interfaces/thread-message.ts +122 -0
- package/src/interfaces/unit-of-work.ts +28 -0
- package/src/types.ts +494 -0
- package/src/wellknown.ts +85 -0
- package/tsconfig.json +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@remit/data-ports",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"default": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"./conformance": {
|
|
13
|
+
"types": "./src/conformance/index.ts",
|
|
14
|
+
"default": "./src/conformance/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"./errors": {
|
|
17
|
+
"types": "./src/errors.ts",
|
|
18
|
+
"default": "./src/errors.ts"
|
|
19
|
+
},
|
|
20
|
+
"./id": {
|
|
21
|
+
"types": "./src/id.ts",
|
|
22
|
+
"default": "./src/id.ts"
|
|
23
|
+
},
|
|
24
|
+
"./account-settings": {
|
|
25
|
+
"types": "./src/account-settings.ts",
|
|
26
|
+
"default": "./src/account-settings.ts"
|
|
27
|
+
},
|
|
28
|
+
"./wellknown": {
|
|
29
|
+
"types": "./src/wellknown.ts",
|
|
30
|
+
"default": "./src/wellknown.ts"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test:typecheck": "tsgo --noEmit"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@remit/domain-enums": "*",
|
|
38
|
+
"short-uuid": "*",
|
|
39
|
+
"uuid": "*",
|
|
40
|
+
"zod": "*"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@remit/api-zod-schemas": "*"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/remit-mail/remit.git",
|
|
52
|
+
"directory": "packages/data-ports"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { AccountSettingName } from "@remit/domain-enums";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export type AccountSettingNameValue =
|
|
5
|
+
(typeof AccountSettingName)[keyof typeof AccountSettingName];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Per-`kind` value schemas, one per variant of the `AccountSettingValue`
|
|
9
|
+
* discriminated union in TypeSpec (`AccountSetting.tsp`). Authored here because
|
|
10
|
+
* the registry is the binding authority for what a value must satisfy — these
|
|
11
|
+
* are the contract every read and write is validated against.
|
|
12
|
+
*/
|
|
13
|
+
export const BooleanSettingSchema = z.object({
|
|
14
|
+
kind: z.literal("Boolean"),
|
|
15
|
+
value: z.boolean(),
|
|
16
|
+
});
|
|
17
|
+
export const StringSettingSchema = z.object({
|
|
18
|
+
kind: z.literal("String"),
|
|
19
|
+
value: z.string(),
|
|
20
|
+
});
|
|
21
|
+
export const NumberSettingSchema = z.object({
|
|
22
|
+
kind: z.literal("Number"),
|
|
23
|
+
value: z.number(),
|
|
24
|
+
});
|
|
25
|
+
export const StringListSettingSchema = z.object({
|
|
26
|
+
kind: z.literal("StringList"),
|
|
27
|
+
value: z.array(z.string()),
|
|
28
|
+
});
|
|
29
|
+
export const MapSettingSchema = z.object({
|
|
30
|
+
kind: z.literal("Map"),
|
|
31
|
+
value: z.record(z.string(), z.string()),
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* MutedFlag-valued setting. The value is a rich mute object (`MutedFlag` in
|
|
35
|
+
* TypeSpec / `@remit/api-openapi-types`): a boolean plus audit metadata. The
|
|
36
|
+
* schema is authored here — generated packages ship only `.d.ts` for this shape,
|
|
37
|
+
* so the registry owns the runtime contract a relocated mute flag is validated
|
|
38
|
+
* against (RFC 032 lesson from the signature slice).
|
|
39
|
+
*/
|
|
40
|
+
export const MutedFlagSettingSchema = z.object({
|
|
41
|
+
kind: z.literal("MutedFlag"),
|
|
42
|
+
value: z.object({
|
|
43
|
+
value: z.boolean(),
|
|
44
|
+
setAt: z.number(),
|
|
45
|
+
setBy: z.string().optional(),
|
|
46
|
+
expiresAt: z.number().optional(),
|
|
47
|
+
reason: z.string().optional(),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* RFC 032 Tier 1 registry: binds each setting `name` to the zod schema its
|
|
53
|
+
* `value` must satisfy. The discriminated union (`kind`) types the shape on the
|
|
54
|
+
* wire; this registry binds a name to one concrete shape, so a read or write of
|
|
55
|
+
* a given setting is validated against exactly the variant it requires.
|
|
56
|
+
*
|
|
57
|
+
* Adding a setting key is a deliberate change: extend the `AccountSettingName`
|
|
58
|
+
* enum in TypeSpec and add an entry here — never an ad-hoc, unvalidated value.
|
|
59
|
+
*/
|
|
60
|
+
export const accountSettingRegistry = {
|
|
61
|
+
[AccountSettingName.Theme]: StringSettingSchema,
|
|
62
|
+
[AccountSettingName.Density]: StringSettingSchema,
|
|
63
|
+
[AccountSettingName.DefaultComposerFormat]: StringSettingSchema,
|
|
64
|
+
[AccountSettingName.PinnedFolders]: StringListSettingSchema,
|
|
65
|
+
[AccountSettingName.AccountSignaturePlainText]: StringSettingSchema,
|
|
66
|
+
[AccountSettingName.AccountSignatureHtml]: StringSettingSchema,
|
|
67
|
+
[AccountSettingName.AccountDisplayName]: StringSettingSchema,
|
|
68
|
+
[AccountSettingName.AccountMuted]: MutedFlagSettingSchema,
|
|
69
|
+
[AccountSettingName.MailboxDisplayName]: StringSettingSchema,
|
|
70
|
+
[AccountSettingName.MailboxMuted]: MutedFlagSettingSchema,
|
|
71
|
+
[AccountSettingName.FolderRoleAppointment]: StringSettingSchema,
|
|
72
|
+
/**
|
|
73
|
+
* Deprecated tombstone: superseded by `FolderRoleAppointment` (RFC 032
|
|
74
|
+
* exclusive-folder-appointment, #976). Kept only so `baseSettingName`
|
|
75
|
+
* recognises leftover `MailboxRole#<mailboxId>` rows written by the #963/#964
|
|
76
|
+
* backfill instead of throwing `Unknown account setting name`; the read-side
|
|
77
|
+
* groupers (`groupAccountOverrides`, `groupMailboxOverrides`,
|
|
78
|
+
* `groupFolderAppointmentsByAccount`) already skip any base they don't
|
|
79
|
+
* explicitly handle, so a `MailboxRole` row is ignored, not surfaced. Do not
|
|
80
|
+
* write new rows under this name. Remove once the row-deletion migration ships.
|
|
81
|
+
*/
|
|
82
|
+
[AccountSettingName.MailboxRole]: StringSettingSchema,
|
|
83
|
+
} as const satisfies Record<AccountSettingNameValue, z.ZodTypeAny>;
|
|
84
|
+
|
|
85
|
+
export type AccountSettingRegistry = typeof accountSettingRegistry;
|
|
86
|
+
|
|
87
|
+
export type AccountSettingValueFor<N extends AccountSettingNameValue> = z.infer<
|
|
88
|
+
AccountSettingRegistry[N]
|
|
89
|
+
>;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Separator between a setting's base enum member and its per-target suffix in a
|
|
93
|
+
* composite stored name (e.g. `AccountSignaturePlainText#<accountId>`). Some
|
|
94
|
+
* settings are per-account, but `AccountSetting` is keyed per accountConfigId, so
|
|
95
|
+
* the stored `name` encodes the target after this separator. The registry still
|
|
96
|
+
* validates by the base member; readers filter by the suffix.
|
|
97
|
+
*/
|
|
98
|
+
export const SETTING_NAME_SEPARATOR = "#";
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A persisted setting `name`. Either a plain closed-enum member or a composite
|
|
102
|
+
* `<base>#<suffix>` for a per-target setting. Always a `string` whose first
|
|
103
|
+
* segment is a known base member.
|
|
104
|
+
*/
|
|
105
|
+
export type StoredSettingName = string;
|
|
106
|
+
|
|
107
|
+
/** Compose the stored name for a per-target setting (e.g. one signature per account). */
|
|
108
|
+
export function composeSettingName(
|
|
109
|
+
base: AccountSettingNameValue,
|
|
110
|
+
target: string,
|
|
111
|
+
): StoredSettingName {
|
|
112
|
+
return `${base}${SETTING_NAME_SEPARATOR}${target}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const isAccountSettingNameValue = (
|
|
116
|
+
value: string,
|
|
117
|
+
): value is AccountSettingNameValue =>
|
|
118
|
+
Object.hasOwn(accountSettingRegistry, value);
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Derive the base enum member from a stored name. Strips the `#<suffix>` from a
|
|
122
|
+
* composite name; returns a plain name unchanged. Throws when the base segment
|
|
123
|
+
* is not a registered setting (let-it-crash — an unknown key is a programmer
|
|
124
|
+
* error, never an ad-hoc column).
|
|
125
|
+
*/
|
|
126
|
+
export function baseSettingName(
|
|
127
|
+
name: StoredSettingName,
|
|
128
|
+
): AccountSettingNameValue {
|
|
129
|
+
const base = name.split(SETTING_NAME_SEPARATOR)[0] ?? "";
|
|
130
|
+
if (!isAccountSettingNameValue(base)) {
|
|
131
|
+
throw new Error(`Unknown account setting name: ${name}`);
|
|
132
|
+
}
|
|
133
|
+
return base;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Every value shape the registry can hold. Useful where a value is handled
|
|
138
|
+
* generically (e.g. the persisted `value` attribute) rather than per-name.
|
|
139
|
+
*/
|
|
140
|
+
export type AnyAccountSettingValue = z.infer<
|
|
141
|
+
| typeof BooleanSettingSchema
|
|
142
|
+
| typeof StringSettingSchema
|
|
143
|
+
| typeof NumberSettingSchema
|
|
144
|
+
| typeof StringListSettingSchema
|
|
145
|
+
| typeof MapSettingSchema
|
|
146
|
+
| typeof MutedFlagSettingSchema
|
|
147
|
+
>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Parse-and-narrow a value against the schema registered for `name`. Accepts a
|
|
151
|
+
* plain enum member or a composite `<base>#<suffix>` name; validation resolves
|
|
152
|
+
* to the base member's schema. Throws a ZodError on mismatch (let-it-crash);
|
|
153
|
+
* returns the value typed to the variant the name requires.
|
|
154
|
+
*/
|
|
155
|
+
export function parseAccountSettingValue<N extends AccountSettingNameValue>(
|
|
156
|
+
name: N,
|
|
157
|
+
value: unknown,
|
|
158
|
+
): AccountSettingValueFor<N>;
|
|
159
|
+
export function parseAccountSettingValue(
|
|
160
|
+
name: StoredSettingName,
|
|
161
|
+
value: unknown,
|
|
162
|
+
): AnyAccountSettingValue;
|
|
163
|
+
export function parseAccountSettingValue(
|
|
164
|
+
name: StoredSettingName,
|
|
165
|
+
value: unknown,
|
|
166
|
+
): AnyAccountSettingValue {
|
|
167
|
+
const schema = accountSettingRegistry[baseSettingName(name)];
|
|
168
|
+
return schema.parse(value) as AnyAccountSettingValue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Non-throwing variant: returns the zod SafeParse result so a caller with a
|
|
173
|
+
* recovery path can branch on `success`. Accepts composite names like above.
|
|
174
|
+
*/
|
|
175
|
+
export function safeParseAccountSettingValue<N extends AccountSettingNameValue>(
|
|
176
|
+
name: N,
|
|
177
|
+
value: unknown,
|
|
178
|
+
): z.SafeParseReturnType<unknown, AccountSettingValueFor<N>>;
|
|
179
|
+
export function safeParseAccountSettingValue(
|
|
180
|
+
name: StoredSettingName,
|
|
181
|
+
value: unknown,
|
|
182
|
+
): z.SafeParseReturnType<unknown, AnyAccountSettingValue>;
|
|
183
|
+
export function safeParseAccountSettingValue(
|
|
184
|
+
name: StoredSettingName,
|
|
185
|
+
value: unknown,
|
|
186
|
+
): z.SafeParseReturnType<unknown, AnyAccountSettingValue> {
|
|
187
|
+
const schema = accountSettingRegistry[baseSettingName(name)];
|
|
188
|
+
return schema.safeParse(value) as z.SafeParseReturnType<
|
|
189
|
+
unknown,
|
|
190
|
+
AnyAccountSettingValue
|
|
191
|
+
>;
|
|
192
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A backend's binding for a conformance suite: how to build the repository
|
|
3
|
+
* under test, how to tear its backing store down, and the two behaviours the
|
|
4
|
+
* suite cannot express portably — id minting and not-found detection, both of
|
|
5
|
+
* which differ per implementation.
|
|
6
|
+
*/
|
|
7
|
+
export interface RepositoryConformanceHarness<TRepo> {
|
|
8
|
+
createRepository(): Promise<TRepo>;
|
|
9
|
+
teardown(): Promise<void>;
|
|
10
|
+
makeId(): string;
|
|
11
|
+
isNotFoundError(error: unknown): boolean;
|
|
12
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { after, before, describe, test } from "node:test";
|
|
3
|
+
import { LabelColor } from "@remit/domain-enums";
|
|
4
|
+
import type { ILabelRepository } from "../interfaces/label.js";
|
|
5
|
+
import type { RepositoryConformanceHarness } from "./harness.js";
|
|
6
|
+
|
|
7
|
+
export function labelRepositoryConformance(
|
|
8
|
+
harness: RepositoryConformanceHarness<ILabelRepository>,
|
|
9
|
+
): void {
|
|
10
|
+
describe("ILabelRepository conformance", () => {
|
|
11
|
+
let repo: ILabelRepository;
|
|
12
|
+
|
|
13
|
+
before(async () => {
|
|
14
|
+
repo = await harness.createRepository();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
after(() => harness.teardown());
|
|
18
|
+
|
|
19
|
+
test("create derives normalizedName and defaults color", async () => {
|
|
20
|
+
const accountConfigId = harness.makeId();
|
|
21
|
+
|
|
22
|
+
const label = await repo.create({
|
|
23
|
+
accountConfigId,
|
|
24
|
+
name: " Receipts ",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.equal(label.name, " Receipts ");
|
|
28
|
+
assert.equal(label.normalizedName, "receipts");
|
|
29
|
+
assert.equal(label.color, LabelColor.Default);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("get throws a not-found error for a missing label", async () => {
|
|
33
|
+
await assert.rejects(
|
|
34
|
+
repo.get(harness.makeId(), harness.makeId()),
|
|
35
|
+
(error) => harness.isNotFoundError(error),
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("update re-derives normalizedName when the name changes", async () => {
|
|
40
|
+
const accountConfigId = harness.makeId();
|
|
41
|
+
const label = await repo.create({ accountConfigId, name: "Old Name" });
|
|
42
|
+
|
|
43
|
+
const updated = await repo.update(accountConfigId, label.labelId, {
|
|
44
|
+
name: "New Name",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
assert.equal(updated.name, "New Name");
|
|
48
|
+
assert.equal(updated.normalizedName, "new name");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("update throws a not-found error for a missing label", async () => {
|
|
52
|
+
await assert.rejects(
|
|
53
|
+
repo.update(harness.makeId(), harness.makeId(), { name: "x" }),
|
|
54
|
+
(error) => harness.isNotFoundError(error),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("listByAccountConfig scopes to the account", async () => {
|
|
59
|
+
const accountConfigId = harness.makeId();
|
|
60
|
+
const other = harness.makeId();
|
|
61
|
+
|
|
62
|
+
const mine = await repo.create({ accountConfigId, name: "Mine" });
|
|
63
|
+
await repo.create({ accountConfigId: other, name: "Foreign" });
|
|
64
|
+
|
|
65
|
+
const labels = await repo.listByAccountConfig(accountConfigId);
|
|
66
|
+
assert.equal(labels.length, 1);
|
|
67
|
+
assert.equal(labels[0]?.labelId, mine.labelId);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("findByNormalizedName dedupes case- and whitespace-insensitively", async () => {
|
|
71
|
+
const accountConfigId = harness.makeId();
|
|
72
|
+
const label = await repo.create({ accountConfigId, name: "Work" });
|
|
73
|
+
|
|
74
|
+
const found = await repo.findByNormalizedName(accountConfigId, "WORK");
|
|
75
|
+
assert.equal(found?.labelId, label.labelId);
|
|
76
|
+
|
|
77
|
+
const missing = await repo.findByNormalizedName(
|
|
78
|
+
accountConfigId,
|
|
79
|
+
"nonexistent",
|
|
80
|
+
);
|
|
81
|
+
assert.equal(missing, null);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("delete removes the row", async () => {
|
|
85
|
+
const accountConfigId = harness.makeId();
|
|
86
|
+
const label = await repo.create({ accountConfigId, name: "Gone" });
|
|
87
|
+
|
|
88
|
+
await repo.delete(accountConfigId, label.labelId);
|
|
89
|
+
|
|
90
|
+
await assert.rejects(repo.get(accountConfigId, label.labelId), (error) =>
|
|
91
|
+
harness.isNotFoundError(error),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export class HTTPError extends Error {
|
|
2
|
+
public statusCode = 500;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export class BadRequestError extends HTTPError {
|
|
6
|
+
name = "BadRequestError";
|
|
7
|
+
public statusCode = 400;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class ClientError extends HTTPError {
|
|
11
|
+
name = "ClientError";
|
|
12
|
+
public statusCode = 401;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class ForbiddenError extends HTTPError {
|
|
16
|
+
name = "ForbiddenError";
|
|
17
|
+
public statusCode = 403;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class NotFoundError extends HTTPError {
|
|
21
|
+
name = "NotFoundError";
|
|
22
|
+
public statusCode = 404;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ConflictError extends HTTPError {
|
|
26
|
+
name = "ConflictError";
|
|
27
|
+
public statusCode = 409;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A message exists but its body could not be fetched/parsed after every
|
|
32
|
+
* body-sync retry was spent (issue #1270 / epic #1281 invariant 3). This is
|
|
33
|
+
* distinct from `NotFoundError`: the message is real, but its content is
|
|
34
|
+
* permanently unavailable — the client must show an explicit, actionable
|
|
35
|
+
* error instead of retrying (no 202, no silent skip).
|
|
36
|
+
*/
|
|
37
|
+
export class UnrecoverableBodyError extends HTTPError {
|
|
38
|
+
name = "UnrecoverableBodyError";
|
|
39
|
+
public statusCode = 422;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class CreateFailedConflictError extends ConflictError {
|
|
43
|
+
public statusCode = 409;
|
|
44
|
+
name = "CreateFailedConflictError";
|
|
45
|
+
constructor(resourceType: string, params: unknown) {
|
|
46
|
+
super(
|
|
47
|
+
`${resourceType} with properties "${JSON.stringify(params)}" already exists.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class UnhandledError extends HTTPError {
|
|
53
|
+
name = "UnhandledError";
|
|
54
|
+
public statusCode = 500;
|
|
55
|
+
readonly cause: Error | undefined;
|
|
56
|
+
|
|
57
|
+
constructor(message: string, cause?: Error) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.message = message;
|
|
60
|
+
if (cause) this.cause = cause;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
toJSON() {
|
|
64
|
+
return {
|
|
65
|
+
message: this.message,
|
|
66
|
+
cause: this.cause?.message,
|
|
67
|
+
stack: this.cause?.stack || this.stack,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class InternalServerError extends UnhandledError {
|
|
73
|
+
name = "InternalServerError";
|
|
74
|
+
public statusCode = 500;
|
|
75
|
+
}
|
package/src/id.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import shortUuid from "short-uuid";
|
|
2
|
+
import { v5 as uuidv5 } from "uuid";
|
|
3
|
+
import type { MessageIdSource } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const translator = shortUuid.createTranslator(shortUuid.constants.uuid25Base36);
|
|
6
|
+
|
|
7
|
+
export const base36uuid = (): string => translator.generate();
|
|
8
|
+
|
|
9
|
+
export const fromUUID = (uuid: string): string => translator.fromUUID(uuid);
|
|
10
|
+
|
|
11
|
+
export const toUUID = (shortId: string): string => translator.toUUID(shortId);
|
|
12
|
+
|
|
13
|
+
export const generateUuidv5 = (name: string, namespace: string): string =>
|
|
14
|
+
uuidv5(name, namespace);
|
|
15
|
+
|
|
16
|
+
export const base36uuidv5 = (name: string, namespace: string): string =>
|
|
17
|
+
translator.fromUUID(uuidv5(name, namespace));
|
|
18
|
+
|
|
19
|
+
export const REMIT_NAMESPACE = "9e89694d-214b-4d9b-99f5-214b4d9b99f5";
|
|
20
|
+
|
|
21
|
+
export const deriveAddressId = (
|
|
22
|
+
accountConfigId: string,
|
|
23
|
+
email: string,
|
|
24
|
+
): string =>
|
|
25
|
+
base36uuidv5(
|
|
26
|
+
`address:${accountConfigId}:${email.toLowerCase()}`,
|
|
27
|
+
REMIT_NAMESPACE,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export const deriveEnvelopeAddressId = (
|
|
31
|
+
messageId: string,
|
|
32
|
+
role: string,
|
|
33
|
+
order: number,
|
|
34
|
+
): string =>
|
|
35
|
+
base36uuidv5(
|
|
36
|
+
`envelopeaddress:${messageId}:${role}:${order}`,
|
|
37
|
+
REMIT_NAMESPACE,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
export const isValidMessageId = (messageId: string | undefined): boolean => {
|
|
41
|
+
if (!messageId) return false;
|
|
42
|
+
const trimmed = messageId.trim();
|
|
43
|
+
return trimmed !== "" && trimmed !== "<>";
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const normalizeMessageIdHeader = (source: MessageIdSource): string => {
|
|
47
|
+
if (isValidMessageId(source.messageId)) {
|
|
48
|
+
return source.messageId as string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const parts = [
|
|
52
|
+
"generated",
|
|
53
|
+
source.mailboxId,
|
|
54
|
+
source.uid.toString(),
|
|
55
|
+
source.date || "",
|
|
56
|
+
source.subject || "",
|
|
57
|
+
source.fromMailbox || "",
|
|
58
|
+
source.fromHost || "",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
return parts.join(":");
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const deriveMessageId = (
|
|
65
|
+
accountId: string,
|
|
66
|
+
messageIdHeader: string,
|
|
67
|
+
): string =>
|
|
68
|
+
base36uuidv5(`message:${accountId}:${messageIdHeader}`, REMIT_NAMESPACE);
|
|
69
|
+
|
|
70
|
+
export const deriveMessageIdFromSource = (
|
|
71
|
+
accountId: string,
|
|
72
|
+
source: MessageIdSource,
|
|
73
|
+
): string => deriveMessageId(accountId, normalizeMessageIdHeader(source));
|
|
74
|
+
|
|
75
|
+
export const deriveThreadId = (
|
|
76
|
+
accountId: string,
|
|
77
|
+
rootMessageIdHeader: string,
|
|
78
|
+
): string =>
|
|
79
|
+
base36uuidv5(
|
|
80
|
+
`thread:${accountId}:${rootMessageIdHeader.toLowerCase()}`,
|
|
81
|
+
REMIT_NAMESPACE,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export const deriveEnvelopeId = (messageId: string): string =>
|
|
85
|
+
base36uuidv5(`envelope:${messageId}`, REMIT_NAMESPACE);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Canonical IMAP path for the root MIME node. BODYSTRUCTURE leaves the
|
|
89
|
+
* root's `part` blank; we assign "0" so it has a stable, unambiguous key
|
|
90
|
+
* across the codebase (BodyPart row id, S3 key, seed scripts).
|
|
91
|
+
*/
|
|
92
|
+
export const ROOT_PART_PATH = "0";
|
|
93
|
+
|
|
94
|
+
export const deriveBodyPartId = (messageId: string, partPath: string): string =>
|
|
95
|
+
base36uuidv5(`bodypart:${messageId}:${partPath}`, REMIT_NAMESPACE);
|
|
96
|
+
|
|
97
|
+
export const deriveBodyPartParameterId = (
|
|
98
|
+
messageId: string,
|
|
99
|
+
partPath: string,
|
|
100
|
+
parameterName: string,
|
|
101
|
+
): string =>
|
|
102
|
+
base36uuidv5(
|
|
103
|
+
`bodypartparam:${messageId}:${partPath}:${parameterName.toLowerCase()}`,
|
|
104
|
+
REMIT_NAMESPACE,
|
|
105
|
+
);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export type { IAccountRepository } from "./interfaces/account.js";
|
|
2
|
+
export type { IAccountConfigRepository } from "./interfaces/account-config.js";
|
|
3
|
+
export type { IAccountExportRequestRepository } from "./interfaces/account-export-request.js";
|
|
4
|
+
export type { IAccountSettingRepository } from "./interfaces/account-setting.js";
|
|
5
|
+
export type { IAddressRepository } from "./interfaces/address.js";
|
|
6
|
+
export type { IEnvelopeRepository } from "./interfaces/envelope.js";
|
|
7
|
+
export type { IFilterRepository } from "./interfaces/filter.js";
|
|
8
|
+
export type { IFilterAnchorRepository } from "./interfaces/filter-anchor.js";
|
|
9
|
+
export type { ILabelRepository } from "./interfaces/label.js";
|
|
10
|
+
export type { IMailboxRepository } from "./interfaces/mailbox.js";
|
|
11
|
+
export type { IMailboxLockRepository } from "./interfaces/mailbox-lock.js";
|
|
12
|
+
export type { IMailboxSpecialUseRepository } from "./interfaces/mailbox-special-use.js";
|
|
13
|
+
export type { IMessageRepository } from "./interfaces/message.js";
|
|
14
|
+
export type { IMessageFlagRepository } from "./interfaces/message-flag.js";
|
|
15
|
+
export type { IMessageFlagPushRepository } from "./interfaces/message-flag-push.js";
|
|
16
|
+
export type { IMessageLabelRepository } from "./interfaces/message-label.js";
|
|
17
|
+
export type { IMessagePlacementMoveRepository } from "./interfaces/message-placement-move.js";
|
|
18
|
+
export type { IOrganizeJobRequestRepository } from "./interfaces/organize-job-request.js";
|
|
19
|
+
export type { IOutboxMessageRepository } from "./interfaces/outbox-message.js";
|
|
20
|
+
export type { IThreadMessageRepository } from "./interfaces/thread-message.js";
|
|
21
|
+
export type {
|
|
22
|
+
IUnitOfWork,
|
|
23
|
+
UnitOfWorkRepositories,
|
|
24
|
+
} from "./interfaces/unit-of-work.js";
|
|
25
|
+
export type {
|
|
26
|
+
AccountConfigDescription,
|
|
27
|
+
AccountConfigItem,
|
|
28
|
+
AccountDescription,
|
|
29
|
+
AccountExportRequestItem,
|
|
30
|
+
AccountItem,
|
|
31
|
+
AccountSchedulerPage,
|
|
32
|
+
AccountSettingItem,
|
|
33
|
+
AccountSettingValue,
|
|
34
|
+
AddressFlags,
|
|
35
|
+
AddressItem,
|
|
36
|
+
BodyPartContentItem,
|
|
37
|
+
BodyPartContentUpsertInput,
|
|
38
|
+
BodyPartItem,
|
|
39
|
+
BodyPartParameterItem,
|
|
40
|
+
BodyPartParameterUpsertInput,
|
|
41
|
+
BodyPartStorageItem,
|
|
42
|
+
BodyPartUpsertInput,
|
|
43
|
+
CreateAccountConfigInput,
|
|
44
|
+
CreateAccountExportRequestInput,
|
|
45
|
+
CreateAccountInput,
|
|
46
|
+
CreateAddressInput,
|
|
47
|
+
CreateEnvelopeAddressInput,
|
|
48
|
+
CreateEnvelopeInput,
|
|
49
|
+
CreateFilterAnchorInput,
|
|
50
|
+
CreateFilterInput,
|
|
51
|
+
CreateLabelInput,
|
|
52
|
+
CreateMailboxInput,
|
|
53
|
+
CreateMessageFlagInput,
|
|
54
|
+
CreateMessageInput,
|
|
55
|
+
CreateMessageLabelInput,
|
|
56
|
+
CreateOrganizeJobRequestInput,
|
|
57
|
+
CreateOutboxMessageInput,
|
|
58
|
+
CreateThreadMessageInput,
|
|
59
|
+
EnvelopeAddressItem,
|
|
60
|
+
EnvelopeItem,
|
|
61
|
+
FilterAnchorItem,
|
|
62
|
+
FilterItem,
|
|
63
|
+
FlagsMergePatch,
|
|
64
|
+
LabelItem,
|
|
65
|
+
ListOptions,
|
|
66
|
+
MailboxItem,
|
|
67
|
+
MailboxLockItem,
|
|
68
|
+
MailboxSpecialUseItem,
|
|
69
|
+
MailboxSpecialUseValue,
|
|
70
|
+
MessageData,
|
|
71
|
+
MessageDescription,
|
|
72
|
+
MessageFlagItem,
|
|
73
|
+
MessageFlagPushItem,
|
|
74
|
+
MessageIdSource,
|
|
75
|
+
MessageItem,
|
|
76
|
+
MessageLabelItem,
|
|
77
|
+
MessagePlacementMoveItem,
|
|
78
|
+
MessageReferenceItem,
|
|
79
|
+
OrganizeJobRequestItem,
|
|
80
|
+
OutboxMessageItem,
|
|
81
|
+
PutMessageFlagPushInput,
|
|
82
|
+
PutMessagePlacementMoveInput,
|
|
83
|
+
RawMessageStorageItem,
|
|
84
|
+
ResultList,
|
|
85
|
+
SearchOptions,
|
|
86
|
+
ThreadMessageItem,
|
|
87
|
+
UpdateAccountConfigInput,
|
|
88
|
+
UpdateAccountExportRequestInput,
|
|
89
|
+
UpdateAccountInput,
|
|
90
|
+
UpdateAddressInput,
|
|
91
|
+
UpdateEnvelopeInput,
|
|
92
|
+
UpdateFilterInput,
|
|
93
|
+
UpdateLabelInput,
|
|
94
|
+
UpdateMailboxInput,
|
|
95
|
+
UpdateMessageInput,
|
|
96
|
+
UpdateMessageMoveInput,
|
|
97
|
+
UpdateOrganizeJobRequestInput,
|
|
98
|
+
UpdateOutboxMessageInput,
|
|
99
|
+
UpdateThreadMessageInput,
|
|
100
|
+
UpsertAccountSettingInput,
|
|
101
|
+
WithMailboxLockResult,
|
|
102
|
+
} from "./types.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountConfigDescription,
|
|
3
|
+
AccountConfigItem,
|
|
4
|
+
CreateAccountConfigInput,
|
|
5
|
+
ResultList,
|
|
6
|
+
UpdateAccountConfigInput,
|
|
7
|
+
} from "../types.js";
|
|
8
|
+
|
|
9
|
+
export interface IAccountConfigRepository {
|
|
10
|
+
create(input: CreateAccountConfigInput): Promise<AccountConfigItem>;
|
|
11
|
+
get(accountConfigId: string): Promise<AccountConfigItem>;
|
|
12
|
+
get(accountConfigIds: string[]): Promise<AccountConfigItem[]>;
|
|
13
|
+
update(
|
|
14
|
+
accountConfigId: string,
|
|
15
|
+
input: UpdateAccountConfigInput,
|
|
16
|
+
): Promise<AccountConfigItem>;
|
|
17
|
+
delete(accountConfigId: string): Promise<void>;
|
|
18
|
+
deleteMany(accountConfigIds: string[]): Promise<void>;
|
|
19
|
+
listByUser(
|
|
20
|
+
userId: string,
|
|
21
|
+
options?: { limit?: number; continuationToken?: string },
|
|
22
|
+
): Promise<ResultList<AccountConfigItem>>;
|
|
23
|
+
describe(accountConfigId: string): Promise<AccountConfigDescription>;
|
|
24
|
+
listAll(): Promise<AccountConfigItem[]>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountExportRequestItem,
|
|
3
|
+
CreateAccountExportRequestInput,
|
|
4
|
+
ResultList,
|
|
5
|
+
UpdateAccountExportRequestInput,
|
|
6
|
+
} from "../types.js";
|
|
7
|
+
|
|
8
|
+
export interface IAccountExportRequestRepository {
|
|
9
|
+
create(
|
|
10
|
+
input: CreateAccountExportRequestInput,
|
|
11
|
+
): Promise<AccountExportRequestItem>;
|
|
12
|
+
get(accountExportRequestId: string): Promise<AccountExportRequestItem>;
|
|
13
|
+
update(
|
|
14
|
+
accountExportRequestId: string,
|
|
15
|
+
input: UpdateAccountExportRequestInput,
|
|
16
|
+
): Promise<AccountExportRequestItem>;
|
|
17
|
+
listByAccountConfig(
|
|
18
|
+
accountConfigId: string,
|
|
19
|
+
options?: { limit?: number; continuationToken?: string },
|
|
20
|
+
): Promise<ResultList<AccountExportRequestItem>>;
|
|
21
|
+
}
|