better-inbox 0.0.1 → 0.1.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/README.md +130 -2
- package/dist/client.cjs +41 -0
- package/dist/client.d.cts +19 -0
- package/dist/client.d.ts +19 -0
- package/dist/client.js +16 -0
- package/dist/index.cjs +299 -0
- package/dist/index.d.cts +291 -0
- package/dist/index.d.ts +291 -0
- package/dist/index.js +263 -0
- package/dist/react/index.cjs +301 -0
- package/dist/react/index.d.cts +96 -0
- package/dist/react/index.d.ts +96 -0
- package/dist/react/index.js +272 -0
- package/llms.txt +47 -0
- package/package.json +59 -5
- package/index.js +0 -3
package/README.md
CHANGED
|
@@ -1,5 +1,133 @@
|
|
|
1
1
|
# better-inbox
|
|
2
2
|
|
|
3
|
-
In-app
|
|
3
|
+
In-app notifications for [better-auth](https://better-auth.com) apps. One plugin, one migration, one component — notifications live in **your** database, addressed to **your** users.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```
|
|
6
|
+
npm install better-inbox
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
- **A better-auth plugin, not a platform.** No services to deploy, no dashboard SaaS. The `notification` table lives in your own database via better-auth's adapter (drizzle, prisma, kysely — whatever you already use).
|
|
10
|
+
- **Addressed to users, not email addresses.** `notify({ userId })` uses your better-auth user ids. Delete a user, their notifications cascade.
|
|
11
|
+
- **Organization-aware.** Notify a whole org — optionally filtered by role — via the better-auth organization plugin: `notify({ organizationId, roles: ["owner", "admin"] })`.
|
|
12
|
+
- **One React component.** `<InboxButton />` gives you the bell, unread badge, and inbox panel, styled with shadcn CSS variables (works with any shadcn theme, zero runtime dependencies, no shadcn required).
|
|
13
|
+
|
|
14
|
+
> Community plugin — not affiliated with the better-auth team.
|
|
15
|
+
|
|
16
|
+
## Quickstart
|
|
17
|
+
|
|
18
|
+
**1. Add the plugin** to your better-auth config:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// lib/auth.ts
|
|
22
|
+
import { betterAuth } from "better-auth";
|
|
23
|
+
import { inbox } from "better-inbox";
|
|
24
|
+
|
|
25
|
+
export const auth = betterAuth({
|
|
26
|
+
// ...your existing config
|
|
27
|
+
plugins: [inbox()],
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**2. Migrate.** The plugin declares the `notification` table; your existing better-auth workflow creates it:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
npx @better-auth/cli migrate --config lib/auth.ts # or: generate, for drizzle/prisma
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
(Point `--config` at your better-auth config if the CLI doesn't auto-detect it.)
|
|
38
|
+
|
|
39
|
+
**3. Add the client plugin:**
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// lib/auth-client.ts
|
|
43
|
+
import { createAuthClient } from "better-auth/react";
|
|
44
|
+
import { inboxClient } from "better-inbox/client";
|
|
45
|
+
|
|
46
|
+
export const authClient = createAuthClient({
|
|
47
|
+
plugins: [inboxClient()],
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**4. Drop in the component** (any client component, e.g. your navbar):
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
"use client";
|
|
55
|
+
import { InboxButton } from "better-inbox/react";
|
|
56
|
+
import { useRouter } from "next/navigation";
|
|
57
|
+
import { authClient } from "@/lib/auth-client";
|
|
58
|
+
|
|
59
|
+
export function Navbar() {
|
|
60
|
+
const router = useRouter();
|
|
61
|
+
return <InboxButton client={authClient} onNavigate={(href) => router.push(href)} />;
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**5. Send notifications** from any server code — a server action, a webhook handler, a cron job:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
await auth.api.notify({
|
|
69
|
+
body: {
|
|
70
|
+
userId: comment.authorId,
|
|
71
|
+
type: "comment.reply",
|
|
72
|
+
title: `${user.name} replied to your comment`,
|
|
73
|
+
href: `/posts/${post.id}#comment-${comment.id}`,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`notify` is server-only: callable through `auth.api`, never exposed as an HTTP route.
|
|
79
|
+
|
|
80
|
+
## Notify a whole organization
|
|
81
|
+
|
|
82
|
+
With the better-auth [organization plugin](https://better-auth.com/docs/plugins/organization) enabled, address an org instead of a user. One notification is created per member (fan-out on write), each stamped with `organizationId`:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// Stripe webhook: payment failed → tell the org's admins
|
|
86
|
+
await auth.api.notify({
|
|
87
|
+
body: {
|
|
88
|
+
organizationId: subscription.metadata.orgId,
|
|
89
|
+
roles: ["owner", "admin"], // optional — omit to notify every member
|
|
90
|
+
type: "billing.payment_failed",
|
|
91
|
+
title: "Payment failed — update your card",
|
|
92
|
+
href: "/settings/billing",
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Fan-out is capped at 1,000 members by default (`inbox({ maxFanout })` to change). Over the cap, `notify` throws instead of hammering your database.
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
### Server (`better-inbox`)
|
|
102
|
+
|
|
103
|
+
| Endpoint | Access | Description |
|
|
104
|
+
|----------|--------|-------------|
|
|
105
|
+
| `auth.api.notify({ body })` | server-only | Create notification(s). Exactly one of `userId` / `organizationId`; optional `roles`, `body`, `href`, `data` (JSON) |
|
|
106
|
+
| `auth.api.listNotifications({ query })` | session | `{ filter?: "unread"\|"all", limit? (≤100), offset?, organizationId? }` → `{ notifications, hasMore }` |
|
|
107
|
+
| `auth.api.markRead({ body: { id } })` | session | Marks one of the caller's notifications read |
|
|
108
|
+
| `auth.api.markAllRead({ body })` | session | Optional `{ organizationId }` scope |
|
|
109
|
+
| `auth.api.unreadCount({ query })` | session | → `{ count }` |
|
|
110
|
+
|
|
111
|
+
Every session endpoint is scoped to the caller — users can only ever see and mutate their own notifications.
|
|
112
|
+
|
|
113
|
+
### React (`better-inbox/react`)
|
|
114
|
+
|
|
115
|
+
- `<InboxButton client={authClient} />` — bell + badge + panel. Props: `onNavigate`, `renderItem`, `pollInterval` (default 30s; unread count only, full refresh on window focus and panel open), `organizationId`, `className`.
|
|
116
|
+
- `useInbox(client, options)` — build your own UI: `{ notifications, unreadCount, isLoading, hasMore, loadMore, markRead, markAllRead, refresh }`.
|
|
117
|
+
- `<InboxPanel inbox={useInbox(...)} />` — the panel without the bell.
|
|
118
|
+
|
|
119
|
+
## Performance note
|
|
120
|
+
|
|
121
|
+
Add an index for the list query once you have real traffic — plugin schemas can't declare indexes, so it's one manual line, e.g. drizzle:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
index("notification_user_created_idx").on(table.userId, table.createdAt)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## What this is not (yet)
|
|
128
|
+
|
|
129
|
+
Email/push channels, preferences, digests, realtime websockets — out of scope for v0.1 by design. Polling + focus refetch covers the badge honestly. If you need multi-channel delivery pipelines, look at [Novu](https://novu.co) or [betternotify](https://github.com/better-notify/better-notify); better-inbox is the thin, DB-owned layer for the in-app half.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT
|
package/dist/client.cjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/client.ts
|
|
21
|
+
var client_exports = {};
|
|
22
|
+
__export(client_exports, {
|
|
23
|
+
inboxClient: () => inboxClient
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(client_exports);
|
|
26
|
+
var inboxClient = () => {
|
|
27
|
+
return {
|
|
28
|
+
id: "inbox",
|
|
29
|
+
$InferServerPlugin: {},
|
|
30
|
+
pathMethods: {
|
|
31
|
+
"/inbox/list": "GET",
|
|
32
|
+
"/inbox/unread-count": "GET",
|
|
33
|
+
"/inbox/mark-read": "POST",
|
|
34
|
+
"/inbox/mark-all-read": "POST"
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
39
|
+
0 && (module.exports = {
|
|
40
|
+
inboxClient
|
|
41
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { inbox } from './index.cjs';
|
|
2
|
+
export { Notification } from './index.cjs';
|
|
3
|
+
import 'better-auth';
|
|
4
|
+
import 'better-call';
|
|
5
|
+
import 'zod/v4/core';
|
|
6
|
+
import 'zod';
|
|
7
|
+
|
|
8
|
+
declare const inboxClient: () => {
|
|
9
|
+
id: "inbox";
|
|
10
|
+
$InferServerPlugin: ReturnType<typeof inbox>;
|
|
11
|
+
pathMethods: {
|
|
12
|
+
"/inbox/list": "GET";
|
|
13
|
+
"/inbox/unread-count": "GET";
|
|
14
|
+
"/inbox/mark-read": "POST";
|
|
15
|
+
"/inbox/mark-all-read": "POST";
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { inboxClient };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { inbox } from './index.js';
|
|
2
|
+
export { Notification } from './index.js';
|
|
3
|
+
import 'better-auth';
|
|
4
|
+
import 'better-call';
|
|
5
|
+
import 'zod/v4/core';
|
|
6
|
+
import 'zod';
|
|
7
|
+
|
|
8
|
+
declare const inboxClient: () => {
|
|
9
|
+
id: "inbox";
|
|
10
|
+
$InferServerPlugin: ReturnType<typeof inbox>;
|
|
11
|
+
pathMethods: {
|
|
12
|
+
"/inbox/list": "GET";
|
|
13
|
+
"/inbox/unread-count": "GET";
|
|
14
|
+
"/inbox/mark-read": "POST";
|
|
15
|
+
"/inbox/mark-all-read": "POST";
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { inboxClient };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
var inboxClient = () => {
|
|
3
|
+
return {
|
|
4
|
+
id: "inbox",
|
|
5
|
+
$InferServerPlugin: {},
|
|
6
|
+
pathMethods: {
|
|
7
|
+
"/inbox/list": "GET",
|
|
8
|
+
"/inbox/unread-count": "GET",
|
|
9
|
+
"/inbox/mark-read": "POST",
|
|
10
|
+
"/inbox/mark-all-read": "POST"
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export {
|
|
15
|
+
inboxClient
|
|
16
|
+
};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
INBOX_ERROR_CODES: () => INBOX_ERROR_CODES,
|
|
34
|
+
inbox: () => inbox
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(src_exports);
|
|
37
|
+
var import_db = require("better-auth/db");
|
|
38
|
+
|
|
39
|
+
// src/error-codes.ts
|
|
40
|
+
var import_better_auth = require("better-auth");
|
|
41
|
+
var INBOX_ERROR_CODES = (0, import_better_auth.defineErrorCodes)({
|
|
42
|
+
NOTIFICATION_NOT_FOUND: "Notification not found",
|
|
43
|
+
USER_OR_ORGANIZATION_REQUIRED: "Provide exactly one of userId or organizationId",
|
|
44
|
+
ORGANIZATION_PLUGIN_REQUIRED: "organizationId requires the organization plugin",
|
|
45
|
+
ORGANIZATION_HAS_NO_MEMBERS: "Organization has no matching members",
|
|
46
|
+
FAN_OUT_LIMIT_EXCEEDED: "Organization member count exceeds maxFanout"
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// src/routes.ts
|
|
50
|
+
var import_api = require("better-auth/api");
|
|
51
|
+
var z = __toESM(require("zod"), 1);
|
|
52
|
+
var notifyBodySchema = z.object({
|
|
53
|
+
userId: z.string().optional(),
|
|
54
|
+
organizationId: z.string().optional(),
|
|
55
|
+
roles: z.array(z.string()).optional(),
|
|
56
|
+
type: z.string(),
|
|
57
|
+
title: z.string(),
|
|
58
|
+
body: z.string().optional(),
|
|
59
|
+
href: z.string().optional(),
|
|
60
|
+
data: z.record(z.string(), z.unknown()).optional()
|
|
61
|
+
}).refine((body) => !!body.userId !== !!body.organizationId, {
|
|
62
|
+
message: INBOX_ERROR_CODES.USER_OR_ORGANIZATION_REQUIRED.message
|
|
63
|
+
});
|
|
64
|
+
var notify = (options) => import_api.createAuthEndpoint.serverOnly(
|
|
65
|
+
{
|
|
66
|
+
method: "POST",
|
|
67
|
+
body: notifyBodySchema
|
|
68
|
+
},
|
|
69
|
+
async (ctx) => {
|
|
70
|
+
const { userId, organizationId, roles, ...payload } = ctx.body;
|
|
71
|
+
const createFor = (targetUserId) => ctx.context.adapter.create({
|
|
72
|
+
model: "notification",
|
|
73
|
+
data: {
|
|
74
|
+
userId: targetUserId,
|
|
75
|
+
...organizationId ? { organizationId } : {},
|
|
76
|
+
...payload,
|
|
77
|
+
read: false,
|
|
78
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
if (!organizationId) {
|
|
82
|
+
const notification = await createFor(userId);
|
|
83
|
+
return { count: 1, notifications: [notification] };
|
|
84
|
+
}
|
|
85
|
+
const hasOrgPlugin = ctx.context.options.plugins?.some(
|
|
86
|
+
(plugin) => plugin.id === "organization"
|
|
87
|
+
);
|
|
88
|
+
if (!hasOrgPlugin) {
|
|
89
|
+
throw import_api.APIError.from(
|
|
90
|
+
"BAD_REQUEST",
|
|
91
|
+
INBOX_ERROR_CODES.ORGANIZATION_PLUGIN_REQUIRED
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const members = await ctx.context.adapter.findMany({
|
|
95
|
+
model: "member",
|
|
96
|
+
where: [{ field: "organizationId", value: organizationId }],
|
|
97
|
+
limit: options.maxFanout + 1
|
|
98
|
+
});
|
|
99
|
+
if (members.length > options.maxFanout) {
|
|
100
|
+
throw import_api.APIError.from(
|
|
101
|
+
"BAD_REQUEST",
|
|
102
|
+
INBOX_ERROR_CODES.FAN_OUT_LIMIT_EXCEEDED
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const targets = roles?.length ? members.filter(
|
|
106
|
+
(member) => member.role.split(",").some((role) => roles.includes(role))
|
|
107
|
+
) : members;
|
|
108
|
+
if (targets.length === 0) {
|
|
109
|
+
throw import_api.APIError.from(
|
|
110
|
+
"BAD_REQUEST",
|
|
111
|
+
INBOX_ERROR_CODES.ORGANIZATION_HAS_NO_MEMBERS
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const notifications = [];
|
|
115
|
+
for (const member of targets) {
|
|
116
|
+
notifications.push(await createFor(member.userId));
|
|
117
|
+
}
|
|
118
|
+
return { count: notifications.length, notifications };
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
var listQuerySchema = z.object({
|
|
122
|
+
filter: z.enum(["unread", "all"]).optional(),
|
|
123
|
+
limit: z.coerce.number().int().min(1).max(100).optional(),
|
|
124
|
+
offset: z.coerce.number().int().min(0).optional(),
|
|
125
|
+
organizationId: z.string().optional()
|
|
126
|
+
}).optional();
|
|
127
|
+
var listNotifications = () => (0, import_api.createAuthEndpoint)(
|
|
128
|
+
"/inbox/list",
|
|
129
|
+
{
|
|
130
|
+
method: "GET",
|
|
131
|
+
query: listQuerySchema,
|
|
132
|
+
use: [import_api.sessionMiddleware]
|
|
133
|
+
},
|
|
134
|
+
async (ctx) => {
|
|
135
|
+
const { filter = "all", limit = 20, offset = 0, organizationId } = ctx.query ?? {};
|
|
136
|
+
const where = [
|
|
137
|
+
{ field: "userId", value: ctx.context.session.user.id }
|
|
138
|
+
];
|
|
139
|
+
if (filter === "unread") {
|
|
140
|
+
where.push({ field: "read", value: false });
|
|
141
|
+
}
|
|
142
|
+
if (organizationId) {
|
|
143
|
+
where.push({ field: "organizationId", value: organizationId });
|
|
144
|
+
}
|
|
145
|
+
const rows = await ctx.context.adapter.findMany({
|
|
146
|
+
model: "notification",
|
|
147
|
+
where,
|
|
148
|
+
limit: limit + 1,
|
|
149
|
+
offset,
|
|
150
|
+
sortBy: { field: "createdAt", direction: "desc" }
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
notifications: rows.slice(0, limit),
|
|
154
|
+
hasMore: rows.length > limit
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
var markRead = () => (0, import_api.createAuthEndpoint)(
|
|
159
|
+
"/inbox/mark-read",
|
|
160
|
+
{
|
|
161
|
+
method: "POST",
|
|
162
|
+
body: z.object({ id: z.string() }),
|
|
163
|
+
use: [import_api.sessionMiddleware]
|
|
164
|
+
},
|
|
165
|
+
async (ctx) => {
|
|
166
|
+
const updated = await ctx.context.adapter.update({
|
|
167
|
+
model: "notification",
|
|
168
|
+
where: [
|
|
169
|
+
{ field: "id", value: ctx.body.id },
|
|
170
|
+
{ field: "userId", value: ctx.context.session.user.id }
|
|
171
|
+
],
|
|
172
|
+
update: { read: true }
|
|
173
|
+
});
|
|
174
|
+
if (!updated) {
|
|
175
|
+
throw import_api.APIError.from(
|
|
176
|
+
"NOT_FOUND",
|
|
177
|
+
INBOX_ERROR_CODES.NOTIFICATION_NOT_FOUND
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return { notification: updated };
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
var markAllRead = () => (0, import_api.createAuthEndpoint)(
|
|
184
|
+
"/inbox/mark-all-read",
|
|
185
|
+
{
|
|
186
|
+
method: "POST",
|
|
187
|
+
body: z.object({ organizationId: z.string().optional() }),
|
|
188
|
+
use: [import_api.sessionMiddleware]
|
|
189
|
+
},
|
|
190
|
+
async (ctx) => {
|
|
191
|
+
const where = [
|
|
192
|
+
{ field: "userId", value: ctx.context.session.user.id },
|
|
193
|
+
{ field: "read", value: false }
|
|
194
|
+
];
|
|
195
|
+
if (ctx.body.organizationId) {
|
|
196
|
+
where.push({ field: "organizationId", value: ctx.body.organizationId });
|
|
197
|
+
}
|
|
198
|
+
const count = await ctx.context.adapter.updateMany({
|
|
199
|
+
model: "notification",
|
|
200
|
+
where,
|
|
201
|
+
update: { read: true }
|
|
202
|
+
});
|
|
203
|
+
return { count };
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
var unreadCount = () => (0, import_api.createAuthEndpoint)(
|
|
207
|
+
"/inbox/unread-count",
|
|
208
|
+
{
|
|
209
|
+
method: "GET",
|
|
210
|
+
query: z.object({ organizationId: z.string().optional() }).optional(),
|
|
211
|
+
use: [import_api.sessionMiddleware]
|
|
212
|
+
},
|
|
213
|
+
async (ctx) => {
|
|
214
|
+
const where = [
|
|
215
|
+
{ field: "userId", value: ctx.context.session.user.id },
|
|
216
|
+
{ field: "read", value: false }
|
|
217
|
+
];
|
|
218
|
+
if (ctx.query?.organizationId) {
|
|
219
|
+
where.push({
|
|
220
|
+
field: "organizationId",
|
|
221
|
+
value: ctx.query.organizationId
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const count = await ctx.context.adapter.count({
|
|
225
|
+
model: "notification",
|
|
226
|
+
where
|
|
227
|
+
});
|
|
228
|
+
return { count };
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// src/schema.ts
|
|
233
|
+
var schema = {
|
|
234
|
+
notification: {
|
|
235
|
+
fields: {
|
|
236
|
+
userId: {
|
|
237
|
+
type: "string",
|
|
238
|
+
required: true,
|
|
239
|
+
references: { model: "user", field: "id" }
|
|
240
|
+
},
|
|
241
|
+
organizationId: {
|
|
242
|
+
type: "string",
|
|
243
|
+
required: false
|
|
244
|
+
},
|
|
245
|
+
type: {
|
|
246
|
+
type: "string",
|
|
247
|
+
required: true
|
|
248
|
+
},
|
|
249
|
+
title: {
|
|
250
|
+
type: "string",
|
|
251
|
+
required: true
|
|
252
|
+
},
|
|
253
|
+
body: {
|
|
254
|
+
type: "string",
|
|
255
|
+
required: false
|
|
256
|
+
},
|
|
257
|
+
href: {
|
|
258
|
+
type: "string",
|
|
259
|
+
required: false
|
|
260
|
+
},
|
|
261
|
+
data: {
|
|
262
|
+
type: "json",
|
|
263
|
+
required: false
|
|
264
|
+
},
|
|
265
|
+
read: {
|
|
266
|
+
type: "boolean",
|
|
267
|
+
required: true,
|
|
268
|
+
defaultValue: false
|
|
269
|
+
},
|
|
270
|
+
createdAt: {
|
|
271
|
+
type: "date",
|
|
272
|
+
required: true
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// src/index.ts
|
|
279
|
+
var inbox = (options = {}) => {
|
|
280
|
+
const opts = { maxFanout: options.maxFanout ?? 1e3 };
|
|
281
|
+
return {
|
|
282
|
+
id: "inbox",
|
|
283
|
+
schema: (0, import_db.mergeSchema)(schema, options.schema),
|
|
284
|
+
endpoints: {
|
|
285
|
+
notify: notify(opts),
|
|
286
|
+
listNotifications: listNotifications(),
|
|
287
|
+
markRead: markRead(),
|
|
288
|
+
markAllRead: markAllRead(),
|
|
289
|
+
unreadCount: unreadCount()
|
|
290
|
+
},
|
|
291
|
+
$ERROR_CODES: INBOX_ERROR_CODES,
|
|
292
|
+
options
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
296
|
+
0 && (module.exports = {
|
|
297
|
+
INBOX_ERROR_CODES,
|
|
298
|
+
inbox
|
|
299
|
+
});
|