@12-apps/notifications 4.11.0 → 4.12.0
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/{chunk-6W7INOYQ.js → chunk-CPQKKLPS.js} +1 -1
- package/dist/{chunk-6W7INOYQ.js.map → chunk-CPQKKLPS.js.map} +1 -1
- package/dist/{chunk-ZIR3ILFH.js → chunk-FBBPS2LT.js} +2 -2
- package/dist/{chunk-SWOWHIFE.js → chunk-GK6GSC2J.js} +37 -2
- package/dist/chunk-GK6GSC2J.js.map +1 -0
- package/dist/{chunk-2TJ4D2KE.js → chunk-PNY6S6WH.js} +2 -2
- package/dist/{chunk-WVRODNXQ.js → chunk-WZBX7YCE.js} +32 -15
- package/dist/{chunk-WVRODNXQ.js.map → chunk-WZBX7YCE.js.map} +1 -1
- package/dist/{create-api-notifications-CcPYrM3p.d.ts → create-api-notifications-BTudlaSC.d.ts} +15 -6
- package/dist/{create-web-notifications-CnaXx6km.d.ts → create-web-notifications-2xxbKnrW.d.ts} +2 -2
- package/dist/{generators-qAD4fNPq.d.ts → generators-CQYdJfB5.d.ts} +1 -1
- package/dist/hono/index.d.ts +5 -5
- package/dist/hono/index.js +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +9 -3
- package/dist/{jobs--fex87-q.d.ts → jobs-CaovU4GM.d.ts} +1 -1
- package/dist/manifest/server.d.ts +6 -6
- package/dist/manifest/server.js +4 -4
- package/dist/manifest/web.d.ts +3 -3
- package/dist/manifest/web.js +1 -1
- package/dist/{preferences-screen-SXUIGECY.js → preferences-screen-S3ZHX5LB.js} +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +4 -4
- package/dist/{types-BlqZkCWZ.d.ts → types-H_aFzLA0.d.ts} +33 -0
- package/dist/web-push/index.d.ts +2 -2
- package/dist/{web-push-Dnyaha2z.d.ts → web-push-C6U-5JCV.d.ts} +1 -1
- package/dist/{wire-BG1kuoXX.d.ts → wire-Bn6aA2nL.d.ts} +65 -2
- package/package.json +2 -2
- package/src/index.ts +4 -0
- package/src/preferences-core.ts +107 -0
- package/src/server/preferences.ts +29 -9
- package/src/server/router.ts +37 -7
- package/src/types.ts +33 -0
- package/dist/chunk-SWOWHIFE.js.map +0 -1
- /package/dist/{chunk-ZIR3ILFH.js.map → chunk-FBBPS2LT.js.map} +0 -0
- /package/dist/{chunk-2TJ4D2KE.js.map → chunk-PNY6S6WH.js.map} +0 -0
- /package/dist/{preferences-screen-SXUIGECY.js.map → preferences-screen-S3ZHX5LB.js.map} +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_CHANNEL_ROW,
|
|
3
|
-
|
|
4
|
-
mergeChoices,
|
|
3
|
+
explicitChoicesOf,
|
|
5
4
|
mergeStoredRow,
|
|
5
|
+
resolveTypeChannels,
|
|
6
6
|
type ChannelMatrix,
|
|
7
7
|
type ChannelRow,
|
|
8
|
+
type TypeChannelRules,
|
|
8
9
|
} from '../preferences-core';
|
|
9
10
|
import type {
|
|
10
11
|
NotificationCategory,
|
|
@@ -32,10 +33,20 @@ export interface NotificationPreferenceStore {
|
|
|
32
33
|
userId: string,
|
|
33
34
|
input: Partial<Record<NotificationCategory, Partial<ChannelRow>>>,
|
|
34
35
|
): Promise<void>;
|
|
35
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* The channels enabled for one (user, category) — the router's gate.
|
|
38
|
+
*
|
|
39
|
+
* `rules` carries the emitting TYPE's own declarations (availability and
|
|
40
|
+
* per-type defaults). It is OPTIONAL, and that is what keeps a host store
|
|
41
|
+
* written before it working: a two-parameter implementation is assignable to
|
|
42
|
+
* this signature unchanged, and omitting the argument asks the same question
|
|
43
|
+
* the store has always answered. The router does not rely on a store
|
|
44
|
+
* honouring it — it caps the result by availability itself.
|
|
45
|
+
*/
|
|
36
46
|
enabledChannels(
|
|
37
47
|
userId: string,
|
|
38
48
|
category: NotificationCategory,
|
|
49
|
+
rules?: TypeChannelRules,
|
|
39
50
|
): Promise<NotificationChannel[]>;
|
|
40
51
|
}
|
|
41
52
|
|
|
@@ -72,6 +83,14 @@ export function createPreferenceStore(
|
|
|
72
83
|
* A category outside the taxonomy is IGNORED rather than stored: the DB
|
|
73
84
|
* CHECK would reject it anyway, and a 500 from a stale client's extra key
|
|
74
85
|
* would fail the whole save including the toggle the user did flip.
|
|
86
|
+
*
|
|
87
|
+
* What is written is the user's EXPLICIT choices only, never their
|
|
88
|
+
* effective row. Merging onto the effective row wrote a boolean for all
|
|
89
|
+
* four channels the moment anyone touched any switch, so the row could
|
|
90
|
+
* never again say "no opinion" about a channel — which silently disabled
|
|
91
|
+
* every per-type and per-host default for that user, and defeated the
|
|
92
|
+
* missing-key fallback that lets a new channel ship without a data
|
|
93
|
+
* migration.
|
|
75
94
|
*/
|
|
76
95
|
async save(userId, input) {
|
|
77
96
|
const client = await db();
|
|
@@ -80,8 +99,7 @@ export function createPreferenceStore(
|
|
|
80
99
|
const existing = await client.notificationPreference.findUnique({
|
|
81
100
|
where: { userId_category: { userId, category } },
|
|
82
101
|
});
|
|
83
|
-
const
|
|
84
|
-
const channels = mergeChoices(current, choices);
|
|
102
|
+
const channels = { ...explicitChoicesOf(existing?.channels), ...choices };
|
|
85
103
|
await client.notificationPreference.upsert({
|
|
86
104
|
where: { userId_category: { userId, category } },
|
|
87
105
|
create: { userId, category, channels },
|
|
@@ -90,14 +108,16 @@ export function createPreferenceStore(
|
|
|
90
108
|
}
|
|
91
109
|
},
|
|
92
110
|
|
|
93
|
-
async enabledChannels(userId, category) {
|
|
111
|
+
async enabledChannels(userId, category, rules) {
|
|
94
112
|
const client = await db();
|
|
95
113
|
const row = await client.notificationPreference.findUnique({
|
|
96
114
|
where: { userId_category: { userId, category } },
|
|
97
115
|
});
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
return resolveTypeChannels({
|
|
117
|
+
stored: row?.channels,
|
|
118
|
+
categoryDefaults: defaultRow,
|
|
119
|
+
rules,
|
|
120
|
+
});
|
|
101
121
|
},
|
|
102
122
|
};
|
|
103
123
|
}
|
package/src/server/router.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { UnknownNotificationRecipientError } from '../errors';
|
|
2
2
|
import type { NotificationGeneratorRegistry } from '../generators';
|
|
3
|
-
import { DEFAULT_CHANNEL_ROW, enabledChannelsOf } from '../preferences-core';
|
|
4
|
-
import type {
|
|
3
|
+
import { capToAvailable, DEFAULT_CHANNEL_ROW, enabledChannelsOf } from '../preferences-core';
|
|
4
|
+
import type {
|
|
5
|
+
NotificationChannel,
|
|
6
|
+
NotificationEvent,
|
|
7
|
+
NotificationGenerator,
|
|
8
|
+
TransportRecipient,
|
|
9
|
+
} from '../types';
|
|
5
10
|
|
|
6
11
|
import {
|
|
7
12
|
dispatchOne,
|
|
@@ -174,14 +179,38 @@ function announce(deps: NotificationRouterDeps, notification: CommittedNotificat
|
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
181
|
|
|
177
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* The channels one emit will actually enqueue:
|
|
184
|
+
* preference ∩ transport ∩ plan ∩ the TYPE's own availability.
|
|
185
|
+
*
|
|
186
|
+
* Availability is applied LAST rather than left to the preference store,
|
|
187
|
+
* because {@link NotificationChannelPolicy} is a HOST function returning an
|
|
188
|
+
* array and nothing constrains it to a subset of what it was handed. A policy
|
|
189
|
+
* that returns a channel it was not given — a host reading the plan's own
|
|
190
|
+
* entitlement list rather than filtering the argument — would otherwise put
|
|
191
|
+
* back a channel this type had just declared it does not offer. The final
|
|
192
|
+
* filter makes availability independent of what any host code returns.
|
|
193
|
+
*
|
|
194
|
+
* {@link policyFallback}, by contrast, cannot reintroduce anything: it filters
|
|
195
|
+
* its own already-capped input. It is not a reason for this filter, and the
|
|
196
|
+
* cost of the filter is one pass over at most four strings.
|
|
197
|
+
*
|
|
198
|
+
* The inbox record is NOT gated by any of this — `commit` writes it whatever
|
|
199
|
+
* this returns, including the empty array — because the inbox is the record of
|
|
200
|
+
* what happened rather than a channel a user opts out of.
|
|
201
|
+
*/
|
|
178
202
|
async function resolveChannels(
|
|
179
203
|
deps: NotificationRouterDeps,
|
|
180
204
|
event: NotificationEvent<unknown>,
|
|
181
|
-
|
|
205
|
+
generator: Pick<NotificationGenerator<never>, 'category' | 'channels' | 'channelDefaults'>,
|
|
182
206
|
recipient: TransportRecipient,
|
|
183
207
|
): Promise<NotificationChannel[]> {
|
|
184
|
-
const
|
|
208
|
+
const rules = { channels: generator.channels, channelDefaults: generator.channelDefaults };
|
|
209
|
+
const enabled = await deps.preferences.enabledChannels(
|
|
210
|
+
event.recipient.userId,
|
|
211
|
+
generator.category,
|
|
212
|
+
rules,
|
|
213
|
+
);
|
|
185
214
|
const supported = enabled.filter((channel) => {
|
|
186
215
|
const transport = deps.transports.get(channel);
|
|
187
216
|
return transport !== null && transport.supports(recipient);
|
|
@@ -189,7 +218,8 @@ async function resolveChannels(
|
|
|
189
218
|
// The host's plan gate: a tenant-scoped emit keeps only the channels the
|
|
190
219
|
// tenant's plan covers, so a revoked transport DEGRADES to the remaining ones
|
|
191
220
|
// rather than dropping the notification silently.
|
|
192
|
-
|
|
221
|
+
const permitted = await applyPolicy(deps, event.recipient.clientId, supported);
|
|
222
|
+
return capToAvailable(permitted, generator.channels);
|
|
193
223
|
}
|
|
194
224
|
|
|
195
225
|
/** Commit the inbox record and its delivery rows together, in one transaction. */
|
|
@@ -256,7 +286,7 @@ export function createNotificationRouter(deps: NotificationRouterDeps): Notifica
|
|
|
256
286
|
// generator owns the fallback, in one place a reader can find.
|
|
257
287
|
const content = generator.generate(event.payload as never, { locale: recipient.locale });
|
|
258
288
|
|
|
259
|
-
const channels = await resolveChannels(deps, event, generator
|
|
289
|
+
const channels = await resolveChannels(deps, event, generator, recipient);
|
|
260
290
|
const notification = await commit(deps, event, generator.category, content, channels);
|
|
261
291
|
|
|
262
292
|
// AFTER the transaction, never inside it: a subscriber woken by an event
|
package/src/types.ts
CHANGED
|
@@ -130,6 +130,39 @@ export interface NotificationGenerator<TPayload = unknown> {
|
|
|
130
130
|
type: string;
|
|
131
131
|
/** The preference category the router gates this type's fan-out on. */
|
|
132
132
|
category: NotificationCategory;
|
|
133
|
+
/**
|
|
134
|
+
* The channels this type may EVER use — AVAILABILITY, not preference.
|
|
135
|
+
*
|
|
136
|
+
* Absent (the default) means every channel, which is what every generator
|
|
137
|
+
* written before this field keeps doing. A declared list is a hard cap the
|
|
138
|
+
* router applies AFTER preferences: a user whose stored row explicitly
|
|
139
|
+
* enables a channel this type does not offer still does not get it, because
|
|
140
|
+
* the channel was never on offer for this message. That is the difference
|
|
141
|
+
* from {@link channelDefaults} — a default is a starting point a user can
|
|
142
|
+
* move, availability is the set of starting points that exist.
|
|
143
|
+
*
|
|
144
|
+
* It is per TYPE because the category is too coarse to say it: the three
|
|
145
|
+
* comanda kitchen messages sit in `orders` next to `order.paid`, and a diner
|
|
146
|
+
* three metres from the food wants a push, not correspondence, while the
|
|
147
|
+
* buyer of a delivery order still wants the e-mail. Declaring `["WEB_PUSH"]`
|
|
148
|
+
* on the mesa messages says that without splitting the category or taking
|
|
149
|
+
* `order.paid`'s e-mail away with it.
|
|
150
|
+
*
|
|
151
|
+
* Unknown entries are ignored and order is irrelevant — the list is coerced
|
|
152
|
+
* onto {@link NOTIFICATION_CHANNELS}. An EMPTY list is legal and means no
|
|
153
|
+
* transport channel at all; the inbox record is written regardless, because
|
|
154
|
+
* the inbox is not a channel a user opts out of.
|
|
155
|
+
*/
|
|
156
|
+
channels?: readonly NotificationChannel[];
|
|
157
|
+
/**
|
|
158
|
+
* This type's starting toggles, overriding the category's defaults for the
|
|
159
|
+
* channels it names and only where the user has made NO explicit choice.
|
|
160
|
+
*
|
|
161
|
+
* Stored preferences still win over it — that is what makes it a default.
|
|
162
|
+
* To take a channel away from a user who asked for it, declare
|
|
163
|
+
* {@link channels} instead.
|
|
164
|
+
*/
|
|
165
|
+
channelDefaults?: Partial<Record<NotificationChannel, boolean>>;
|
|
133
166
|
/**
|
|
134
167
|
* Render this event's content for ONE recipient.
|
|
135
168
|
*
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/generators.ts","../src/preferences-core.ts","../src/phone.ts","../src/wire.ts"],"sourcesContent":["/** Thrown by `notify` when no generator is registered for the event type. */\nexport class UnknownNotificationTypeError extends Error {\n readonly type: string;\n constructor(type: string) {\n super(`No notification generator registered for type \"${type}\".`);\n this.name = 'UnknownNotificationTypeError';\n this.type = type;\n Object.setPrototypeOf(this, UnknownNotificationTypeError.prototype);\n }\n}\n\n/** Thrown by `notify` when the recipient has no contact record in the host. */\nexport class UnknownNotificationRecipientError extends Error {\n readonly userId: string;\n constructor(userId: string) {\n super(`notify(): unknown recipient user \"${userId}\".`);\n this.name = 'UnknownNotificationRecipientError';\n this.userId = userId;\n Object.setPrototypeOf(this, UnknownNotificationRecipientError.prototype);\n }\n}\n","import { UnknownNotificationTypeError } from './errors';\nimport type { NotificationGenerator } from './types';\n\n/**\n * Generator registry: one {@link NotificationGenerator} per event `type`.\n *\n * INSTANCE state, not a module-level Map. the origin's own was\n * process-wide, which is what a package of loose functions forces; a factory\n * config does not need it, and one registry per mount is what makes a test (or\n * a second mount) able to hold its own set without clearing anyone else's.\n * Domain modules still register from the OUTSIDE — that is the open/closed\n * seam the whole pipeline is built on — either through the server config's\n * `generators` array or through `registerGenerator` for a late arrival.\n */\nexport interface NotificationGeneratorRegistry {\n /** Register (last-wins, so a re-import is idempotent). */\n register<TPayload>(generator: NotificationGenerator<TPayload>): void;\n /** Resolve for `type`, throwing {@link UnknownNotificationTypeError}. */\n resolve(type: string): NotificationGenerator<never>;\n /** Whether a generator is registered for `type` (emit-site guard). */\n has(type: string): boolean;\n /** Every registered type, for diagnostics. */\n types(): string[];\n}\n\nexport function createGeneratorRegistry(\n initial: readonly NotificationGenerator<never>[] = [],\n): NotificationGeneratorRegistry {\n const generators = new Map<string, NotificationGenerator<never>>();\n const registry: NotificationGeneratorRegistry = {\n register(generator) {\n generators.set(generator.type, generator as NotificationGenerator<never>);\n },\n resolve(type) {\n const generator = generators.get(type);\n if (!generator) throw new UnknownNotificationTypeError(type);\n return generator;\n },\n has: (type) => generators.has(type),\n types: () => [...generators.keys()],\n };\n for (const generator of initial) registry.register(generator);\n return registry;\n}\n","import {\n NOTIFICATION_CHANNELS,\n type NotificationCategory,\n type NotificationChannel,\n} from './types';\n\n/**\n * The preference POLICY, with no storage in it (12-15): which channels a\n * category defaults to, how a stored JSON row is coerced onto the closed\n * channel set, and how a partial save merges. `./server`'s store is the only\n * thing that touches a database, so every rule here is unit-testable without\n * one — and the react half can render the same defaults before the first read\n * lands.\n *\n * Storage stores only EXPLICIT choices (one row per (user, category)); a\n * missing row — or a missing channel key inside a row — falls back to\n * {@link DEFAULT_CHANNEL_ROW}. Defaults: the free, low-friction channels\n * (e-mail + web push) on; the paid per-message channels (SMS + WhatsApp) off\n * until the user opts in. A host that disagrees passes `channelDefaults`.\n */\n\n/** One category's channel toggles. */\nexport type ChannelRow = Record<NotificationChannel, boolean>;\n\n/** A user's full category × channel matrix. */\nexport type ChannelMatrix = Record<NotificationCategory, ChannelRow>;\n\n/** The policy applied when a user never touched a category's toggles. */\nexport const DEFAULT_CHANNEL_ROW: ChannelRow = {\n EMAIL: true,\n SMS: false,\n WHATSAPP: false,\n WEB_PUSH: true,\n};\n\n/** The default matrix for one taxonomy (what the settings UI starts from). */\nexport function defaultChannelMatrix(\n categories: readonly NotificationCategory[],\n channelDefaults: Partial<ChannelRow> = {},\n): ChannelMatrix {\n const row = { ...DEFAULT_CHANNEL_ROW, ...channelDefaults };\n return Object.fromEntries(\n categories.map((category) => [category, { ...row }]),\n ) as ChannelMatrix;\n}\n\n/**\n * Coerce a stored JSON `channels` map onto the closed channel set, filling the\n * gaps from `base`. A stored row that predates a channel keeps that channel's\n * default rather than reading as \"off\", which is what lets a new transport ship\n * without a data migration.\n *\n * The consequence, and the rule it implies: a channel ADDED later turns itself\n * ON for a user who had explicitly switched every channel in that category off,\n * because their stored row has no key for it. That is harmless for the four\n * shipped channels — the two that cost money default off — so **a new channel\n * must be added with a `false` default** unless the user's existing consent\n * already covers it. The alternative (reading a missing key as \"off\") would need\n * a data migration for every existing row on every channel that ever ships.\n */\nexport function mergeStoredRow(stored: unknown, base: ChannelRow): ChannelRow {\n const row = { ...base };\n if (stored && typeof stored === 'object') {\n const record = stored as Record<string, unknown>;\n for (const channel of NOTIFICATION_CHANNELS) {\n const value = record[channel];\n if (typeof value === 'boolean') row[channel] = value;\n }\n }\n return row;\n}\n\n/** The channels enabled by one effective row — the router's gate. */\nexport function enabledChannelsOf(row: ChannelRow): NotificationChannel[] {\n return NOTIFICATION_CHANNELS.filter((channel) => row[channel]);\n}\n\n/**\n * What a PUT writes for one category: the caller's toggles merged over the\n * user's CURRENT effective row. A single-toggle save (how the settings UI\n * writes) must never reset the category's other channels back to their\n * defaults, which is exactly what a whole-row write would do.\n */\nexport function mergeChoices(\n current: ChannelRow,\n choices: Partial<ChannelRow>,\n): ChannelRow {\n return { ...current, ...choices };\n}\n","/**\n * Shared phone-destination rules for the SMS + WhatsApp transports.\n *\n * Providers need E.164 (`+5531999998888`); a host stores the phone as the user\n * entered it. Best-effort normalization: an explicit `+` prefix is trusted; a\n * bare 10/11-digit number is assumed to belong to `defaultCountryCode` and\n * prefixed; anything else is unusable and makes the channel unavailable for\n * that recipient.\n *\n * `defaultCountryCode` is REQUIRED, and that is the whole point of it being a\n * parameter. It used to default to `55` (Brazil, the first host's market),\n * which a published package must not do: a US adopter that never set it turned\n * `4155552671` into `+554155552671` — a plausible Brazilian mobile — and sent a\n * stranger the customer's order details. There is no country this package could\n * assume that is not wrong for every other adopter, so it assumes none and the\n * omission is a compile error rather than a wrong number. the origin passes\n * `'55'` explicitly.\n *\n * NOTE: \"verified phone\" is approximated by \"has a normalizable phone on\n * file\" — a host with a real verification flow should tighten its contact\n * directory to only return verified numbers, which is the single seam both\n * transports funnel through.\n */\n\n/** Options for {@link normalizePhoneE164}. */\nexport interface PhoneNormalizeOptions {\n /**\n * Country calling code for a bare local number, digits only (`'55'`, `'1'`).\n * Required: see the module docstring for why there is no default.\n */\n defaultCountryCode: string;\n}\n\n/** A local subscriber number: area code (2) + 8 or 9 digits. */\nconst isLocal = (digits: string): boolean => digits.length === 10 || digits.length === 11;\n\n/** An already-international number. E.164 allows 8..15 digits. */\nconst international = (digits: string): string | null =>\n digits.length >= 8 && digits.length <= 15 ? `+${digits}` : null;\n\n/** Normalize a stored phone to E.164, or null when it can't be inferred. */\nexport function normalizePhoneE164(\n raw: string | null | undefined,\n options: PhoneNormalizeOptions,\n): string | null {\n if (!raw) return null;\n const country = options.defaultCountryCode;\n const trimmed = raw.trim();\n const digits = trimmed.replace(/\\D/g, '');\n if (trimmed.startsWith('+')) return international(digits);\n if (isLocal(digits)) return `+${country}${digits}`;\n // A bare number that already carries the country code.\n if (digits.startsWith(country) && isLocal(digits.slice(country.length))) {\n return `+${digits}`;\n }\n return null;\n}\n","/**\n * The inbox WIRE shape — the one contract the two halves share.\n *\n * It lives in the root entry rather than in `./server` or `./react` because\n * both halves need it and neither owns it: the api serializes to it, the panel\n * deserializes from it, and a change here is a change to both at once. That is\n * the same reason the response envelope and the route paths are the package's\n * and not the host's.\n */\n\n/** One inbox entry as the notification centre renders it. */\nexport interface InboxNotification {\n id: string;\n type: string;\n category: string;\n title: string;\n body: string;\n link: string | null;\n data: Record<string, unknown>;\n /** ISO-8601, or null while unread. */\n readAt: string | null;\n /** ISO-8601. */\n createdAt: string;\n}\n\n/** One page of the owner's inbox. */\nexport interface ListNotificationsResult {\n items: InboxNotification[];\n /** Cursor for the next page, or null when this page is the last. */\n nextCursor: string | null;\n}\n\n/** A stored notification row, as the db seam hands it back. */\nexport interface NotificationRow {\n id: string;\n userId: string;\n clientId: string | null;\n type: string;\n category: string;\n title: string;\n body: string;\n link: string | null;\n data: unknown;\n readAt: Date | null;\n deletedAt: Date | null;\n createdAt: Date;\n}\n\n/** Row → wire. Dates become ISO strings; a null `data` becomes `{}`. */\nexport function inboxWire(row: NotificationRow): InboxNotification {\n return {\n id: row.id,\n type: row.type,\n category: row.category,\n title: row.title,\n body: row.body,\n link: row.link,\n data: (row.data ?? {}) as Record<string, unknown>,\n readAt: row.readAt ? row.readAt.toISOString() : null,\n createdAt: row.createdAt.toISOString(),\n };\n}\n"],"mappings":"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","names":[]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|