@remit/data-ports 0.0.82 → 0.0.84

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.
Files changed (174) hide show
  1. package/dist/account-settings.d.ts +371 -0
  2. package/dist/account-settings.js +114 -0
  3. package/dist/conformance/account.d.ts +3 -0
  4. package/dist/conformance/account.js +118 -0
  5. package/dist/conformance/calendar-collection.d.ts +3 -0
  6. package/dist/conformance/calendar-collection.js +251 -0
  7. package/dist/conformance/calendar-event-index.d.ts +3 -0
  8. package/dist/conformance/calendar-event-index.js +114 -0
  9. package/dist/conformance/calendar-feed-token.d.ts +3 -0
  10. package/dist/conformance/calendar-feed-token.js +97 -0
  11. package/dist/conformance/calendar-object.d.ts +3 -0
  12. package/dist/conformance/calendar-object.js +156 -0
  13. package/dist/conformance/calendar-suggestion.d.ts +3 -0
  14. package/dist/conformance/calendar-suggestion.js +255 -0
  15. package/{src/conformance/harness.ts → dist/conformance/harness.d.ts} +4 -4
  16. package/dist/conformance/harness.js +1 -0
  17. package/dist/conformance/index.js +8 -0
  18. package/dist/conformance/label.d.ts +3 -0
  19. package/dist/conformance/label.js +92 -0
  20. package/dist/conformance/sender-signer-standing.d.ts +3 -0
  21. package/dist/conformance/sender-signer-standing.js +81 -0
  22. package/dist/display-name.d.ts +1 -0
  23. package/dist/display-name.js +26 -0
  24. package/dist/errors.d.ts +150 -0
  25. package/dist/errors.js +181 -0
  26. package/dist/filter-ttl.d.ts +7 -0
  27. package/{src/filter-ttl.ts → dist/filter-ttl.js} +8 -11
  28. package/dist/folder-role.d.ts +239 -0
  29. package/dist/folder-role.js +276 -0
  30. package/dist/id.d.ts +124 -0
  31. package/{src/id.ts → dist/id.js} +41 -127
  32. package/dist/index.d.ts +37 -0
  33. package/dist/index.js +5 -0
  34. package/dist/interfaces/account-config.d.ts +15 -0
  35. package/dist/interfaces/account-config.js +1 -0
  36. package/dist/interfaces/account-export-request.d.ts +10 -0
  37. package/dist/interfaces/account-export-request.js +1 -0
  38. package/dist/interfaces/account-setting.d.ts +7 -0
  39. package/dist/interfaces/account-setting.js +1 -0
  40. package/dist/interfaces/account.d.ts +29 -0
  41. package/dist/interfaces/account.js +1 -0
  42. package/dist/interfaces/address.d.ts +69 -0
  43. package/dist/interfaces/address.js +1 -0
  44. package/dist/interfaces/calendar-collection.d.ts +38 -0
  45. package/dist/interfaces/calendar-collection.js +1 -0
  46. package/dist/interfaces/calendar-event-index.d.ts +19 -0
  47. package/dist/interfaces/calendar-event-index.js +1 -0
  48. package/dist/interfaces/calendar-feed-token.d.ts +31 -0
  49. package/dist/interfaces/calendar-feed-token.js +1 -0
  50. package/dist/interfaces/calendar-object.d.ts +36 -0
  51. package/dist/interfaces/calendar-object.js +1 -0
  52. package/dist/interfaces/calendar-store.d.ts +93 -0
  53. package/dist/interfaces/calendar-store.js +1 -0
  54. package/dist/interfaces/calendar-suggestion.d.ts +62 -0
  55. package/dist/interfaces/calendar-suggestion.js +1 -0
  56. package/dist/interfaces/config-import.d.ts +8 -0
  57. package/dist/interfaces/config-import.js +1 -0
  58. package/dist/interfaces/envelope.d.ts +24 -0
  59. package/dist/interfaces/envelope.js +1 -0
  60. package/dist/interfaces/filter.d.ts +28 -0
  61. package/dist/interfaces/filter.js +1 -0
  62. package/dist/interfaces/label.d.ts +17 -0
  63. package/dist/interfaces/label.js +1 -0
  64. package/dist/interfaces/mailbox-lock.d.ts +12 -0
  65. package/dist/interfaces/mailbox-lock.js +1 -0
  66. package/dist/interfaces/mailbox-special-use.d.ts +54 -0
  67. package/dist/interfaces/mailbox-special-use.js +1 -0
  68. package/dist/interfaces/mailbox.d.ts +79 -0
  69. package/dist/interfaces/mailbox.js +1 -0
  70. package/dist/interfaces/message-flag-push.d.ts +9 -0
  71. package/dist/interfaces/message-flag-push.js +1 -0
  72. package/dist/interfaces/message-flag.d.ts +14 -0
  73. package/dist/interfaces/message-flag.js +1 -0
  74. package/dist/interfaces/message-label.d.ts +21 -0
  75. package/dist/interfaces/message-label.js +1 -0
  76. package/dist/interfaces/message-placement-move.d.ts +8 -0
  77. package/dist/interfaces/message-placement-move.js +1 -0
  78. package/dist/interfaces/message.d.ts +60 -0
  79. package/dist/interfaces/message.js +1 -0
  80. package/dist/interfaces/organize-job-request.d.ts +10 -0
  81. package/dist/interfaces/organize-job-request.js +1 -0
  82. package/dist/interfaces/outbox-attachment.d.ts +101 -0
  83. package/dist/interfaces/outbox-attachment.js +7 -0
  84. package/dist/interfaces/outbox-message.d.ts +70 -0
  85. package/dist/interfaces/outbox-message.js +20 -0
  86. package/dist/interfaces/quarantine.d.ts +20 -0
  87. package/dist/interfaces/quarantine.js +1 -0
  88. package/dist/interfaces/sender-signer-standing.d.ts +26 -0
  89. package/dist/interfaces/sender-signer-standing.js +1 -0
  90. package/dist/interfaces/sync-warning.d.ts +11 -0
  91. package/dist/interfaces/sync-warning.js +1 -0
  92. package/dist/interfaces/thread-message.d.ts +202 -0
  93. package/dist/interfaces/thread-message.js +1 -0
  94. package/dist/mailbox-name.d.ts +35 -0
  95. package/dist/mailbox-name.js +68 -0
  96. package/dist/message-parts.d.ts +22 -0
  97. package/dist/message-parts.js +18 -0
  98. package/dist/message-settlement.d.ts +35 -0
  99. package/{src/message-settlement.ts → dist/message-settlement.js} +5 -19
  100. package/dist/mutation-events.d.ts +19 -0
  101. package/{src/mutation-events.ts → dist/mutation-events.js} +2 -5
  102. package/dist/sender-mute.d.ts +26 -0
  103. package/{src/sender-mute.ts → dist/sender-mute.js} +12 -34
  104. package/dist/sync-warning-state.d.ts +3 -0
  105. package/dist/sync-warning-state.js +5 -0
  106. package/dist/types.d.ts +491 -0
  107. package/dist/types.js +1 -0
  108. package/dist/update-manifest.d.ts +24 -0
  109. package/dist/update-manifest.js +20 -0
  110. package/dist/virtual-copy.d.ts +8 -0
  111. package/{src/virtual-copy.ts → dist/virtual-copy.js} +10 -15
  112. package/dist/wellknown.d.ts +47 -0
  113. package/{src/wellknown.ts → dist/wellknown.js} +26 -49
  114. package/package.json +44 -79
  115. package/src/account-settings.ts +0 -194
  116. package/src/conformance/account.ts +0 -104
  117. package/src/conformance/calendar-collection.ts +0 -342
  118. package/src/conformance/calendar-event-index.ts +0 -203
  119. package/src/conformance/calendar-feed-token.ts +0 -148
  120. package/src/conformance/calendar-object.ts +0 -269
  121. package/src/conformance/calendar-suggestion.ts +0 -490
  122. package/src/conformance/label.ts +0 -136
  123. package/src/conformance/sender-signer-standing.ts +0 -117
  124. package/src/display-name.test.ts +0 -101
  125. package/src/display-name.ts +0 -40
  126. package/src/errors.ts +0 -226
  127. package/src/folder-role.test.ts +0 -422
  128. package/src/folder-role.ts +0 -447
  129. package/src/id.test.ts +0 -352
  130. package/src/index.ts +0 -170
  131. package/src/interfaces/account-config.ts +0 -25
  132. package/src/interfaces/account-export-request.ts +0 -21
  133. package/src/interfaces/account-setting.ts +0 -14
  134. package/src/interfaces/account.ts +0 -43
  135. package/src/interfaces/address.ts +0 -122
  136. package/src/interfaces/calendar-collection.ts +0 -60
  137. package/src/interfaces/calendar-event-index.ts +0 -34
  138. package/src/interfaces/calendar-feed-token.ts +0 -38
  139. package/src/interfaces/calendar-object.ts +0 -58
  140. package/src/interfaces/calendar-suggestion.ts +0 -93
  141. package/src/interfaces/calendar-unit-of-work.ts +0 -47
  142. package/src/interfaces/config-import.ts +0 -16
  143. package/src/interfaces/envelope.ts +0 -32
  144. package/src/interfaces/filter-anchor-transaction.ts +0 -23
  145. package/src/interfaces/filter-anchor.ts +0 -11
  146. package/src/interfaces/filter.ts +0 -41
  147. package/src/interfaces/label.ts +0 -30
  148. package/src/interfaces/mailbox-lock.ts +0 -32
  149. package/src/interfaces/mailbox-special-use.ts +0 -60
  150. package/src/interfaces/mailbox.ts +0 -115
  151. package/src/interfaces/message-flag-push.ts +0 -17
  152. package/src/interfaces/message-flag.ts +0 -15
  153. package/src/interfaces/message-label.ts +0 -25
  154. package/src/interfaces/message-placement-move.ts +0 -15
  155. package/src/interfaces/message.ts +0 -56
  156. package/src/interfaces/organize-job-request.ts +0 -19
  157. package/src/interfaces/outbox-attachment.ts +0 -132
  158. package/src/interfaces/outbox-message.ts +0 -106
  159. package/src/interfaces/quarantine.ts +0 -22
  160. package/src/interfaces/sender-signer-standing.ts +0 -37
  161. package/src/interfaces/thread-message.ts +0 -264
  162. package/src/interfaces/unit-of-work.ts +0 -28
  163. package/src/mailbox-name.test.ts +0 -106
  164. package/src/mailbox-name.ts +0 -90
  165. package/src/mailbox-state-doors.test.ts +0 -104
  166. package/src/message-placement-doors.test.ts +0 -100
  167. package/src/message-settlement.test.ts +0 -132
  168. package/src/sender-mute.test.ts +0 -76
  169. package/src/types.ts +0 -820
  170. package/src/update-manifest.test.ts +0 -130
  171. package/src/update-manifest.ts +0 -22
  172. package/src/virtual-copy.test.ts +0 -34
  173. package/tsconfig.json +0 -8
  174. /package/{src/conformance/index.ts → dist/conformance/index.d.ts} +0 -0
@@ -20,66 +20,43 @@
20
20
  * existing `lastInboundAt` (the user is replying to an inbound, which
21
21
  * implies a recent inbound, but we still gate on it explicitly).
22
22
  */
23
-
24
23
  export const WELLKNOWN_INBOUND_THRESHOLD = 3;
25
24
  export const WELLKNOWN_REPLY_THRESHOLD = 1;
26
25
  export const WELLKNOWN_INBOUND_WINDOW_MS = 90 * 24 * 60 * 60 * 1000;
27
-
28
26
  /**
29
27
  * Categories and signals that make a sender "bulk". Bulk senders cannot be
30
28
  * promoted by inbound volume alone — only a real reply promotes them.
31
29
  */
32
- const BULK_CATEGORIES: ReadonlySet<string> = new Set([
33
- "newsletter",
34
- "marketing",
35
- "automated",
30
+ const BULK_CATEGORIES = new Set([
31
+ "newsletter",
32
+ "marketing",
33
+ "automated",
36
34
  ]);
37
-
38
- export interface WellknownEngagementSnapshot {
39
- inboundCount?: number;
40
- replyCount?: number;
41
- lastInboundAt?: number;
42
- /**
43
- * Whether this sender is a bulk/automated sender (newsletter, marketing,
44
- * automated category, or List-Unsubscribe header present). Bulk senders
45
- * can only be promoted via `replyCount >= 1`, not by inbound volume.
46
- */
47
- isBulk?: boolean;
48
- flags?: {
49
- wellknown?: { value: boolean } | undefined;
50
- };
51
- }
52
-
53
- export const shouldPromoteWellknown = (
54
- snapshot: WellknownEngagementSnapshot,
55
- now: number,
56
- ): boolean => {
57
- if (snapshot.flags?.wellknown?.value === true) return false;
58
-
59
- const replyCount = snapshot.replyCount ?? 0;
60
- const inboundCount = snapshot.inboundCount ?? 0;
61
- const replied = replyCount >= WELLKNOWN_REPLY_THRESHOLD;
62
- // Bulk senders are only promotable via a real reply, not inbound volume.
63
- const inboundEnough =
64
- !snapshot.isBulk && inboundCount >= WELLKNOWN_INBOUND_THRESHOLD;
65
- if (!replied && !inboundEnough) return false;
66
-
67
- const lastInboundAt = snapshot.lastInboundAt;
68
- if (lastInboundAt === undefined) return false;
69
- if (now - lastInboundAt > WELLKNOWN_INBOUND_WINDOW_MS) return false;
70
-
71
- return true;
35
+ export const shouldPromoteWellknown = (snapshot, now) => {
36
+ if (snapshot.flags?.wellknown?.value === true)
37
+ return false;
38
+ const replyCount = snapshot.replyCount ?? 0;
39
+ const inboundCount = snapshot.inboundCount ?? 0;
40
+ const replied = replyCount >= WELLKNOWN_REPLY_THRESHOLD;
41
+ // Bulk senders are only promotable via a real reply, not inbound volume.
42
+ const inboundEnough = !snapshot.isBulk && inboundCount >= WELLKNOWN_INBOUND_THRESHOLD;
43
+ if (!replied && !inboundEnough)
44
+ return false;
45
+ const lastInboundAt = snapshot.lastInboundAt;
46
+ if (lastInboundAt === undefined)
47
+ return false;
48
+ if (now - lastInboundAt > WELLKNOWN_INBOUND_WINDOW_MS)
49
+ return false;
50
+ return true;
72
51
  };
73
-
74
52
  /**
75
53
  * Returns true when the category or presence of List-Unsubscribe marks the
76
54
  * sender as bulk, meaning they cannot be promoted by inbound volume alone.
77
55
  */
78
- export function isBulkSender(
79
- category: string | undefined,
80
- hasListUnsubscribe: boolean,
81
- ): boolean {
82
- if (hasListUnsubscribe) return true;
83
- if (category != null && BULK_CATEGORIES.has(category)) return true;
84
- return false;
56
+ export function isBulkSender(category, hasListUnsubscribe) {
57
+ if (hasListUnsubscribe)
58
+ return true;
59
+ if (category != null && BULK_CATEGORIES.has(category))
60
+ return true;
61
+ return false;
85
62
  }
package/package.json CHANGED
@@ -1,81 +1,46 @@
1
1
  {
2
- "name": "@remit/data-ports",
3
- "version": "0.0.82",
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
- "./display-name": {
29
- "types": "./src/display-name.ts",
30
- "default": "./src/display-name.ts"
31
- },
32
- "./folder-role": {
33
- "types": "./src/folder-role.ts",
34
- "default": "./src/folder-role.ts"
35
- },
36
- "./mutation-events": {
37
- "types": "./src/mutation-events.ts",
38
- "default": "./src/mutation-events.ts"
39
- },
40
- "./virtual-copy": {
41
- "types": "./src/virtual-copy.ts",
42
- "default": "./src/virtual-copy.ts"
43
- },
44
- "./mailbox-name": {
45
- "types": "./src/mailbox-name.ts",
46
- "default": "./src/mailbox-name.ts"
47
- },
48
- "./wellknown": {
49
- "types": "./src/wellknown.ts",
50
- "default": "./src/wellknown.ts"
51
- },
52
- "./update-manifest": {
53
- "types": "./src/update-manifest.ts",
54
- "default": "./src/update-manifest.ts"
55
- },
56
- "./message-settlement": {
57
- "types": "./src/message-settlement.ts",
58
- "default": "./src/message-settlement.ts"
59
- }
60
- },
61
- "scripts": {
62
- "test:typecheck": "tsgo --noEmit",
63
- "test:run": "node $NODE_TEST_FLAGS --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=85 --test 'src/**/*.test.ts'"
64
- },
65
- "dependencies": {
66
- "@remit/domain-enums": "*",
67
- "short-uuid": "*",
68
- "uuid": "*",
69
- "zod": "^3.25.0",
70
- "@remit/api-zod-schemas": "*"
71
- },
72
- "license": "MIT",
73
- "publishConfig": {
74
- "access": "public"
75
- },
76
- "repository": {
77
- "type": "git",
78
- "url": "git+https://github.com/remit-mail/reader.git",
79
- "directory": "packages/data-ports"
80
- }
2
+ "name": "@remit/data-ports",
3
+ "version": "0.0.84",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./conformance": {
13
+ "types": "./dist/conformance/index.d.ts",
14
+ "default": "./dist/conformance/index.js"
15
+ },
16
+ "./*": {
17
+ "types": "./dist/*.d.ts",
18
+ "default": "./dist/*.js"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "prepack": "node ../../npm-scripts/pack-dist.mjs prepack",
23
+ "postpack": "node ../../npm-scripts/pack-dist.mjs postpack",
24
+ "test:typecheck": "tsgo --noEmit",
25
+ "test:run": "node $NODE_TEST_FLAGS --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/**/*.test.tsx' --test-coverage-lines=85 --test 'src/**/*.test.ts'"
26
+ },
27
+ "dependencies": {
28
+ "@remit/domain-enums": "*",
29
+ "short-uuid": "*",
30
+ "uuid": "*",
31
+ "zod": "^3.25.0",
32
+ "@remit/api-zod-schemas": "*"
33
+ },
34
+ "license": "MIT",
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/remit-mail/reader.git",
41
+ "directory": "packages/data-ports"
42
+ },
43
+ "files": [
44
+ "dist"
45
+ ]
81
46
  }
@@ -1,194 +0,0 @@
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.AccountComposeLanguages]: StringListSettingSchema,
70
- [AccountSettingName.MailboxDisplayName]: StringSettingSchema,
71
- [AccountSettingName.MailboxMuted]: MutedFlagSettingSchema,
72
- [AccountSettingName.FolderRoleAppointment]: StringSettingSchema,
73
- [AccountSettingName.FolderRoleAppointmentLabel]: StringSettingSchema,
74
- /**
75
- * Deprecated tombstone: superseded by `FolderRoleAppointment` (RFC 032
76
- * exclusive-folder-appointment, #976). Kept only so `baseSettingName`
77
- * recognises leftover `MailboxRole#<mailboxId>` rows written by the #963/#964
78
- * backfill instead of throwing `Unknown account setting name`; the read-side
79
- * groupers (`groupAccountOverrides`, `groupMailboxOverrides`,
80
- * `groupFolderAppointmentsByAccount`) already skip any base they don't
81
- * explicitly handle, so a `MailboxRole` row is ignored, not surfaced. Do not
82
- * write new rows under this name. Remove once the row-deletion migration ships.
83
- */
84
- [AccountSettingName.MailboxRole]: StringSettingSchema,
85
- } as const satisfies Record<AccountSettingNameValue, z.ZodTypeAny>;
86
-
87
- export type AccountSettingRegistry = typeof accountSettingRegistry;
88
-
89
- export type AccountSettingValueFor<N extends AccountSettingNameValue> = z.infer<
90
- AccountSettingRegistry[N]
91
- >;
92
-
93
- /**
94
- * Separator between a setting's base enum member and its per-target suffix in a
95
- * composite stored name (e.g. `AccountSignaturePlainText#<accountId>`). Some
96
- * settings are per-account, but `AccountSetting` is keyed per accountConfigId, so
97
- * the stored `name` encodes the target after this separator. The registry still
98
- * validates by the base member; readers filter by the suffix.
99
- */
100
- export const SETTING_NAME_SEPARATOR = "#";
101
-
102
- /**
103
- * A persisted setting `name`. Either a plain closed-enum member or a composite
104
- * `<base>#<suffix>` for a per-target setting. Always a `string` whose first
105
- * segment is a known base member.
106
- */
107
- export type StoredSettingName = string;
108
-
109
- /** Compose the stored name for a per-target setting (e.g. one signature per account). */
110
- export function composeSettingName(
111
- base: AccountSettingNameValue,
112
- target: string,
113
- ): StoredSettingName {
114
- return `${base}${SETTING_NAME_SEPARATOR}${target}`;
115
- }
116
-
117
- const isAccountSettingNameValue = (
118
- value: string,
119
- ): value is AccountSettingNameValue =>
120
- Object.hasOwn(accountSettingRegistry, value);
121
-
122
- /**
123
- * Derive the base enum member from a stored name. Strips the `#<suffix>` from a
124
- * composite name; returns a plain name unchanged. Throws when the base segment
125
- * is not a registered setting (let-it-crash — an unknown key is a programmer
126
- * error, never an ad-hoc column).
127
- */
128
- export function baseSettingName(
129
- name: StoredSettingName,
130
- ): AccountSettingNameValue {
131
- const base = name.split(SETTING_NAME_SEPARATOR)[0] ?? "";
132
- if (!isAccountSettingNameValue(base)) {
133
- throw new Error(`Unknown account setting name: ${name}`);
134
- }
135
- return base;
136
- }
137
-
138
- /**
139
- * Every value shape the registry can hold. Useful where a value is handled
140
- * generically (e.g. the persisted `value` attribute) rather than per-name.
141
- */
142
- export type AnyAccountSettingValue = z.infer<
143
- | typeof BooleanSettingSchema
144
- | typeof StringSettingSchema
145
- | typeof NumberSettingSchema
146
- | typeof StringListSettingSchema
147
- | typeof MapSettingSchema
148
- | typeof MutedFlagSettingSchema
149
- >;
150
-
151
- /**
152
- * Parse-and-narrow a value against the schema registered for `name`. Accepts a
153
- * plain enum member or a composite `<base>#<suffix>` name; validation resolves
154
- * to the base member's schema. Throws a ZodError on mismatch (let-it-crash);
155
- * returns the value typed to the variant the name requires.
156
- */
157
- export function parseAccountSettingValue<N extends AccountSettingNameValue>(
158
- name: N,
159
- value: unknown,
160
- ): AccountSettingValueFor<N>;
161
- export function parseAccountSettingValue(
162
- name: StoredSettingName,
163
- value: unknown,
164
- ): AnyAccountSettingValue;
165
- export function parseAccountSettingValue(
166
- name: StoredSettingName,
167
- value: unknown,
168
- ): AnyAccountSettingValue {
169
- const schema = accountSettingRegistry[baseSettingName(name)];
170
- return schema.parse(value) as AnyAccountSettingValue;
171
- }
172
-
173
- /**
174
- * Non-throwing variant: returns the zod SafeParse result so a caller with a
175
- * recovery path can branch on `success`. Accepts composite names like above.
176
- */
177
- export function safeParseAccountSettingValue<N extends AccountSettingNameValue>(
178
- name: N,
179
- value: unknown,
180
- ): z.SafeParseReturnType<unknown, AccountSettingValueFor<N>>;
181
- export function safeParseAccountSettingValue(
182
- name: StoredSettingName,
183
- value: unknown,
184
- ): z.SafeParseReturnType<unknown, AnyAccountSettingValue>;
185
- export function safeParseAccountSettingValue(
186
- name: StoredSettingName,
187
- value: unknown,
188
- ): z.SafeParseReturnType<unknown, AnyAccountSettingValue> {
189
- const schema = accountSettingRegistry[baseSettingName(name)];
190
- return schema.safeParse(value) as z.SafeParseReturnType<
191
- unknown,
192
- AnyAccountSettingValue
193
- >;
194
- }
@@ -1,104 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { after, before, describe, test } from "node:test";
3
- import { AccountService, ConnectionState } from "@remit/domain-enums";
4
- import type { IAccountRepository } from "../interfaces/account.js";
5
- import type { CreateAccountInput } from "../types.js";
6
- import type { RepositoryConformanceHarness } from "./harness.js";
7
-
8
- const accountInput = (accountConfigId: string): CreateAccountInput => ({
9
- accountConfigId,
10
- username: "person@example.com",
11
- email: "person@example.com",
12
- imapHost: "imap.example.com",
13
- imapPort: 993,
14
- imapTls: true,
15
- imapStartTls: false,
16
- isActive: true,
17
- connectionState: ConnectionState.NotAuthenticated,
18
- });
19
-
20
- export function accountRepositoryConformance(
21
- harness: RepositoryConformanceHarness<IAccountRepository>,
22
- ): void {
23
- describe("IAccountRepository conformance", () => {
24
- let repo: IAccountRepository;
25
-
26
- before(async () => {
27
- repo = await harness.createRepository();
28
- });
29
-
30
- after(() => harness.teardown());
31
-
32
- test("an account created without a service selection syncs mail", async () => {
33
- const created = await repo.create(accountInput(harness.makeId()));
34
-
35
- assert.deepEqual(created.syncedServices, [AccountService.Mail]);
36
- assert.deepEqual((await repo.get(created.accountId)).syncedServices, [
37
- AccountService.Mail,
38
- ]);
39
- });
40
-
41
- test("an account keeps the selection it was created with", async () => {
42
- const created = await repo.create({
43
- ...accountInput(harness.makeId()),
44
- syncedServices: [AccountService.Calendar],
45
- });
46
-
47
- assert.deepEqual((await repo.get(created.accountId)).syncedServices, [
48
- AccountService.Calendar,
49
- ]);
50
- });
51
-
52
- test("an update replaces the selection outright", async () => {
53
- const created = await repo.create(accountInput(harness.makeId()));
54
-
55
- const updated = await repo.update(created.accountId, {
56
- syncedServices: [AccountService.Mail, AccountService.Calendar],
57
- });
58
-
59
- assert.deepEqual(updated.syncedServices, [
60
- AccountService.Mail,
61
- AccountService.Calendar,
62
- ]);
63
- assert.deepEqual((await repo.get(created.accountId)).syncedServices, [
64
- AccountService.Mail,
65
- AccountService.Calendar,
66
- ]);
67
- });
68
-
69
- test("an update that leaves the selection out keeps it", async () => {
70
- const created = await repo.create({
71
- ...accountInput(harness.makeId()),
72
- syncedServices: [AccountService.Calendar],
73
- });
74
-
75
- await repo.update(created.accountId, { lastError: "timeout" });
76
-
77
- assert.deepEqual((await repo.get(created.accountId)).syncedServices, [
78
- AccountService.Calendar,
79
- ]);
80
- });
81
-
82
- test("the mail sources of a config leave out an account with mail off", async () => {
83
- const accountConfigId = harness.makeId();
84
- const mailOnly = await repo.create(accountInput(accountConfigId));
85
- const both = await repo.create({
86
- ...accountInput(accountConfigId),
87
- syncedServices: [AccountService.Mail, AccountService.Calendar],
88
- });
89
- await repo.create({
90
- ...accountInput(accountConfigId),
91
- syncedServices: [AccountService.Calendar],
92
- });
93
- await repo.create(accountInput(harness.makeId()));
94
-
95
- const sources =
96
- await repo.listMailSourcesByAccountConfig(accountConfigId);
97
-
98
- assert.deepEqual(
99
- sources.map((account) => account.accountId).sort(),
100
- [mailOnly.accountId, both.accountId].sort(),
101
- );
102
- });
103
- });
104
- }