@cyguin/notify 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/.github/workflows/publish.yml +27 -0
- package/LICENSE +7 -0
- package/README.md +152 -0
- package/dist/adapters/postgres.cjs +7 -0
- package/dist/adapters/postgres.d.cts +5 -0
- package/dist/adapters/postgres.d.ts +5 -0
- package/dist/adapters/postgres.js +7 -0
- package/dist/adapters/sqlite.cjs +9 -0
- package/dist/adapters/sqlite.d.cts +6 -0
- package/dist/adapters/sqlite.d.ts +6 -0
- package/dist/adapters/sqlite.js +9 -0
- package/dist/chunk-4SP667TN.js +33 -0
- package/dist/chunk-DBFBHOZI.cjs +82 -0
- package/dist/chunk-HW23IB3R.cjs +59 -0
- package/dist/chunk-N3OMUVHL.cjs +33 -0
- package/dist/chunk-NJEVZBJ7.cjs +67 -0
- package/dist/chunk-QHPFQN2C.js +82 -0
- package/dist/chunk-VKPJWS2D.js +59 -0
- package/dist/chunk-WTNWBHMC.js +255 -0
- package/dist/chunk-WZ4RNT3A.js +67 -0
- package/dist/chunk-YWA2XDPM.cjs +255 -0
- package/dist/index.cjs +32 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +32 -0
- package/dist/next.cjs +6 -0
- package/dist/next.d.cts +25 -0
- package/dist/next.d.ts +25 -0
- package/dist/next.js +6 -0
- package/dist/react.cjs +6 -0
- package/dist/react.d.cts +21 -0
- package/dist/react.d.ts +21 -0
- package/dist/react.js +6 -0
- package/dist/types-Q62lBJZ-.d.cts +25 -0
- package/dist/types-Q62lBJZ-.d.ts +25 -0
- package/package.json +71 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/postgres.ts +60 -0
- package/src/adapters/sqlite.ts +82 -0
- package/src/components/NotificationBell.tsx +292 -0
- package/src/components/index.ts +2 -0
- package/src/di.ts +19 -0
- package/src/handlers/route.ts +52 -0
- package/src/index.ts +8 -0
- package/src/notify.ts +7 -0
- package/src/types.ts +22 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +16 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: '20'
|
|
19
|
+
registry-url: 'https://registry.npmjs.org'
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: npm install --legacy-peer-deps
|
|
22
|
+
- name: Build
|
|
23
|
+
run: npm run build
|
|
24
|
+
- name: Publish
|
|
25
|
+
env:
|
|
26
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
27
|
+
run: npm publish --provenance --access public
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 cyguin.com
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @cyguin/notify
|
|
2
|
+
|
|
3
|
+
Server-triggered in-app notifications for Next.js apps.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @cyguin/notify
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
### 1. Create the API route
|
|
14
|
+
|
|
15
|
+
Create `app/api/notify/[...cyguin]/route.ts` in your Next.js app:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { createNotifyHandler } from '@cyguin/notify/next';
|
|
19
|
+
import Database from 'better-sqlite3';
|
|
20
|
+
import { createSQLiteAdapter } from '@cyguin/notify/adapters/sqlite';
|
|
21
|
+
|
|
22
|
+
const db = new Database('notify.db');
|
|
23
|
+
db.pragma('journal_mode = WAL');
|
|
24
|
+
const adapter = createSQLiteAdapter(db);
|
|
25
|
+
|
|
26
|
+
const handler = createNotifyHandler({ adapter });
|
|
27
|
+
|
|
28
|
+
export { handler as GET, handler as POST, handler as PATCH };
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Run migrations
|
|
32
|
+
|
|
33
|
+
```sql
|
|
34
|
+
CREATE TABLE notifications (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
user_id TEXT NOT NULL,
|
|
37
|
+
title TEXT NOT NULL,
|
|
38
|
+
body TEXT NOT NULL,
|
|
39
|
+
href TEXT,
|
|
40
|
+
read_at INTEGER,
|
|
41
|
+
created_at INTEGER NOT NULL
|
|
42
|
+
);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3. Configure the adapter at startup
|
|
46
|
+
|
|
47
|
+
In your app startup (e.g., a setup file or server entry):
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { setNotificationAdapter } from '@cyguin/notify';
|
|
51
|
+
import { createSQLiteAdapter } from '@cyguin/notify/adapters/sqlite';
|
|
52
|
+
|
|
53
|
+
const adapter = createSQLiteAdapter(db);
|
|
54
|
+
setNotificationAdapter(adapter);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 4. Trigger notifications from your server code
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { notify } from '@cyguin/notify';
|
|
61
|
+
|
|
62
|
+
// In a server action, API route, or background job:
|
|
63
|
+
await notify(userId, {
|
|
64
|
+
title: 'Export ready',
|
|
65
|
+
body: 'Your data export is ready to download.',
|
|
66
|
+
href: '/exports/123',
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 5. Add the notification bell
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { NotificationBell } from '@cyguin/notify/react';
|
|
74
|
+
|
|
75
|
+
export default function Header({ user }: { user: { id: string } }) {
|
|
76
|
+
return (
|
|
77
|
+
<header>
|
|
78
|
+
<nav>...</nav>
|
|
79
|
+
<NotificationBell
|
|
80
|
+
userId={user.id}
|
|
81
|
+
pollInterval={30000}
|
|
82
|
+
maxVisible={10}
|
|
83
|
+
onToggle={(open) => console.log('Dropdown:', open)}
|
|
84
|
+
/>
|
|
85
|
+
</header>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## API Routes
|
|
91
|
+
|
|
92
|
+
| Method | Route | Description |
|
|
93
|
+
|--------|-------|-------------|
|
|
94
|
+
| GET | `/api/notify?userId=xxx&limit=N` | List notifications for user |
|
|
95
|
+
| POST | `/api/notify` | Create notification (internal) |
|
|
96
|
+
| PATCH | `/api/notify/:id/read?userId=xxx` | Mark notification as read |
|
|
97
|
+
|
|
98
|
+
## NotificationBell Props
|
|
99
|
+
|
|
100
|
+
| Prop | Default | Description |
|
|
101
|
+
|------|---------|-------------|
|
|
102
|
+
| `userId` | — | User ID to fetch notifications for. |
|
|
103
|
+
| `pollInterval` | `30000` | Polling interval in ms. Pass `0` to disable. |
|
|
104
|
+
| `maxVisible` | `10` | Max notifications to show in dropdown. |
|
|
105
|
+
| `className` | `''` | CSS class for the root element. |
|
|
106
|
+
| `onToggle` | — | Callback when dropdown opens/closes. |
|
|
107
|
+
|
|
108
|
+
## Theming
|
|
109
|
+
|
|
110
|
+
Use `--cyguin-*` CSS custom properties on `.cyguin-notify-bell`:
|
|
111
|
+
|
|
112
|
+
```css
|
|
113
|
+
.cyguin-notify-bell {
|
|
114
|
+
--cyguin-bg: #ffffff;
|
|
115
|
+
--cyguin-fg: #0a0a0a;
|
|
116
|
+
--cyguin-accent: #f5a800;
|
|
117
|
+
--cyguin-border: #e5e5e5;
|
|
118
|
+
--cyguin-radius: 6px;
|
|
119
|
+
--cyguin-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Switch to dark theme with the `theme` prop on the inner button element via CSS:
|
|
124
|
+
|
|
125
|
+
```css
|
|
126
|
+
.cyguin-notify-bell[data-theme="dark"] {
|
|
127
|
+
--cyguin-bg: #0a0a0a;
|
|
128
|
+
--cyguin-bg-subtle: #1a1a1a;
|
|
129
|
+
--cyguin-border: #2a2a2a;
|
|
130
|
+
--cyguin-fg: #f5f5f5;
|
|
131
|
+
--cyguin-shadow: 0 1px 4px rgba(0,0,0,0.4);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Postgres Setup
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { createNotifyHandler } from '@cyguin/notify/next';
|
|
139
|
+
import postgres from 'postgres';
|
|
140
|
+
import { createPostgresAdapter } from '@cyguin/notify/adapters/postgres';
|
|
141
|
+
|
|
142
|
+
const sql = postgres(process.env.DATABASE_URL!);
|
|
143
|
+
const adapter = createPostgresAdapter(sql);
|
|
144
|
+
|
|
145
|
+
const handler = createNotifyHandler({ adapter });
|
|
146
|
+
|
|
147
|
+
export { handler as GET, handler as POST, handler as PATCH };
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true});
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
var _chunkDBFBHOZIcjs = require('../chunk-DBFBHOZI.cjs');
|
|
5
|
+
require('../chunk-N3OMUVHL.cjs');
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
exports.SQLiteNotificationAdapter = _chunkDBFBHOZIcjs.SQLiteNotificationAdapter; exports.createSQLiteAdapter = _chunkDBFBHOZIcjs.createSQLiteAdapter;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// node_modules/nanoid/index.js
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
// node_modules/nanoid/url-alphabet/index.js
|
|
5
|
+
var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
6
|
+
|
|
7
|
+
// node_modules/nanoid/index.js
|
|
8
|
+
var POOL_SIZE_MULTIPLIER = 128;
|
|
9
|
+
var pool;
|
|
10
|
+
var poolOffset;
|
|
11
|
+
var fillPool = (bytes) => {
|
|
12
|
+
if (!pool || pool.length < bytes) {
|
|
13
|
+
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
|
14
|
+
crypto.randomFillSync(pool);
|
|
15
|
+
poolOffset = 0;
|
|
16
|
+
} else if (poolOffset + bytes > pool.length) {
|
|
17
|
+
crypto.randomFillSync(pool);
|
|
18
|
+
poolOffset = 0;
|
|
19
|
+
}
|
|
20
|
+
poolOffset += bytes;
|
|
21
|
+
};
|
|
22
|
+
var nanoid = (size = 21) => {
|
|
23
|
+
fillPool(size |= 0);
|
|
24
|
+
let id = "";
|
|
25
|
+
for (let i = poolOffset - size; i < poolOffset; i++) {
|
|
26
|
+
id += urlAlphabet[pool[i] & 63];
|
|
27
|
+
}
|
|
28
|
+
return id;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
nanoid
|
|
33
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }
|
|
2
|
+
|
|
3
|
+
var _chunkN3OMUVHLcjs = require('./chunk-N3OMUVHL.cjs');
|
|
4
|
+
|
|
5
|
+
// src/adapters/sqlite.ts
|
|
6
|
+
var _bettersqlite3 = require('better-sqlite3'); var _bettersqlite32 = _interopRequireDefault(_bettersqlite3);
|
|
7
|
+
var _db = null;
|
|
8
|
+
function getDb() {
|
|
9
|
+
if (!_db) {
|
|
10
|
+
_db = new (0, _bettersqlite32.default)(":memory:");
|
|
11
|
+
_db.pragma("journal_mode = WAL");
|
|
12
|
+
_db.exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
user_id TEXT NOT NULL,
|
|
16
|
+
title TEXT NOT NULL,
|
|
17
|
+
body TEXT NOT NULL,
|
|
18
|
+
href TEXT,
|
|
19
|
+
read_at INTEGER,
|
|
20
|
+
created_at INTEGER NOT NULL
|
|
21
|
+
);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at);
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
return _db;
|
|
27
|
+
}
|
|
28
|
+
function rowToRecord(row) {
|
|
29
|
+
return {
|
|
30
|
+
id: row.id,
|
|
31
|
+
userId: row.user_id,
|
|
32
|
+
title: row.title,
|
|
33
|
+
body: row.body,
|
|
34
|
+
href: row.href,
|
|
35
|
+
readAt: row.read_at,
|
|
36
|
+
createdAt: row.created_at
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
var SQLiteNotificationAdapter = {
|
|
40
|
+
async create(notification) {
|
|
41
|
+
const db = getDb();
|
|
42
|
+
const id = _chunkN3OMUVHLcjs.nanoid.call(void 0, );
|
|
43
|
+
const createdAt = Date.now();
|
|
44
|
+
db.prepare(`
|
|
45
|
+
INSERT INTO notifications (id, user_id, title, body, href, created_at)
|
|
46
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
47
|
+
`).run(id, notification.userId, notification.title, notification.body, _nullishCoalesce(notification.href, () => ( null)), createdAt);
|
|
48
|
+
return { id, ...notification, createdAt };
|
|
49
|
+
},
|
|
50
|
+
async findManyByUser(userId, { limit = 20, offset = 0 } = {}) {
|
|
51
|
+
const db = getDb();
|
|
52
|
+
const rows = db.prepare(`
|
|
53
|
+
SELECT id, user_id, title, body, href, read_at, created_at
|
|
54
|
+
FROM notifications
|
|
55
|
+
WHERE user_id = ?
|
|
56
|
+
ORDER BY created_at DESC
|
|
57
|
+
LIMIT ? OFFSET ?
|
|
58
|
+
`).all(userId, limit, offset);
|
|
59
|
+
return rows.map(rowToRecord);
|
|
60
|
+
},
|
|
61
|
+
async markRead(id, userId) {
|
|
62
|
+
const db = getDb();
|
|
63
|
+
db.prepare(`
|
|
64
|
+
UPDATE notifications SET read_at = ? WHERE id = ? AND user_id = ?
|
|
65
|
+
`).run(Date.now(), id, userId);
|
|
66
|
+
},
|
|
67
|
+
async countUnread(userId) {
|
|
68
|
+
const db = getDb();
|
|
69
|
+
const row = db.prepare(`
|
|
70
|
+
SELECT COUNT(*) as count FROM notifications WHERE user_id = ? AND read_at IS NULL
|
|
71
|
+
`).get(userId);
|
|
72
|
+
return row.count;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function createSQLiteAdapter() {
|
|
76
|
+
return SQLiteNotificationAdapter;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
exports.SQLiteNotificationAdapter = SQLiteNotificationAdapter; exports.createSQLiteAdapter = createSQLiteAdapter;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
var _chunkN3OMUVHLcjs = require('./chunk-N3OMUVHL.cjs');
|
|
4
|
+
|
|
5
|
+
// src/adapters/postgres.ts
|
|
6
|
+
function createPostgresAdapter(connectionString) {
|
|
7
|
+
const pool = globalThis.__cyguin_postgres_pool;
|
|
8
|
+
const sql = pool ? (q, p) => pool.query(q, p).then((r) => r.rows) : async (_q, _p) => {
|
|
9
|
+
throw new Error("Postgres pool not initialized");
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
async create(notification) {
|
|
13
|
+
const id = _chunkN3OMUVHLcjs.nanoid.call(void 0, );
|
|
14
|
+
const createdAt = Date.now();
|
|
15
|
+
await sql(
|
|
16
|
+
`INSERT INTO notifications (id, user_id, title, body, href, created_at)
|
|
17
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
18
|
+
[id, notification.userId, notification.title, notification.body, _nullishCoalesce(notification.href, () => ( null)), createdAt]
|
|
19
|
+
);
|
|
20
|
+
return { id, ...notification, createdAt };
|
|
21
|
+
},
|
|
22
|
+
async findManyByUser(userId, { limit = 20, offset = 0 } = {}) {
|
|
23
|
+
const rows = await sql(
|
|
24
|
+
`SELECT id, user_id, title, body, href, read_at, created_at
|
|
25
|
+
FROM notifications
|
|
26
|
+
WHERE user_id = $1
|
|
27
|
+
ORDER BY created_at DESC
|
|
28
|
+
LIMIT $2 OFFSET $3`,
|
|
29
|
+
[userId, limit, offset]
|
|
30
|
+
);
|
|
31
|
+
return rows.map((row) => ({
|
|
32
|
+
id: row.id,
|
|
33
|
+
userId: row.user_id,
|
|
34
|
+
title: row.title,
|
|
35
|
+
body: row.body,
|
|
36
|
+
href: row.href,
|
|
37
|
+
readAt: row.read_at,
|
|
38
|
+
createdAt: row.created_at
|
|
39
|
+
}));
|
|
40
|
+
},
|
|
41
|
+
async markRead(id, userId) {
|
|
42
|
+
await sql(
|
|
43
|
+
`UPDATE notifications SET read_at = $1 WHERE id = $2 AND user_id = $3`,
|
|
44
|
+
[Date.now(), id, userId]
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
async countUnread(userId) {
|
|
48
|
+
const rows = await sql(
|
|
49
|
+
`SELECT COUNT(*) as count FROM notifications WHERE user_id = $1 AND read_at IS NULL`,
|
|
50
|
+
[userId]
|
|
51
|
+
);
|
|
52
|
+
return Number(_nullishCoalesce(_optionalChain([rows, 'access', _ => _[0], 'optionalAccess', _2 => _2.count]), () => ( 0)));
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
exports.createPostgresAdapter = createPostgresAdapter;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }// node_modules/nanoid/index.js
|
|
2
|
+
var _crypto = require('crypto'); var _crypto2 = _interopRequireDefault(_crypto);
|
|
3
|
+
|
|
4
|
+
// node_modules/nanoid/url-alphabet/index.js
|
|
5
|
+
var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
6
|
+
|
|
7
|
+
// node_modules/nanoid/index.js
|
|
8
|
+
var POOL_SIZE_MULTIPLIER = 128;
|
|
9
|
+
var pool;
|
|
10
|
+
var poolOffset;
|
|
11
|
+
var fillPool = (bytes) => {
|
|
12
|
+
if (!pool || pool.length < bytes) {
|
|
13
|
+
pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
|
|
14
|
+
_crypto2.default.randomFillSync(pool);
|
|
15
|
+
poolOffset = 0;
|
|
16
|
+
} else if (poolOffset + bytes > pool.length) {
|
|
17
|
+
_crypto2.default.randomFillSync(pool);
|
|
18
|
+
poolOffset = 0;
|
|
19
|
+
}
|
|
20
|
+
poolOffset += bytes;
|
|
21
|
+
};
|
|
22
|
+
var nanoid = (size = 21) => {
|
|
23
|
+
fillPool(size |= 0);
|
|
24
|
+
let id = "";
|
|
25
|
+
for (let i = poolOffset - size; i < poolOffset; i++) {
|
|
26
|
+
id += urlAlphabet[pool[i] & 63];
|
|
27
|
+
}
|
|
28
|
+
return id;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
exports.nanoid = nanoid;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/handlers/route.ts
|
|
2
|
+
var _server = require('next/server');
|
|
3
|
+
|
|
4
|
+
// src/di.ts
|
|
5
|
+
var NOTIFICATION_ADAPTER = /* @__PURE__ */ Symbol.for("@cyguin/notify/NotificationAdapter");
|
|
6
|
+
var _adapter = null;
|
|
7
|
+
function setNotificationAdapter(adapter) {
|
|
8
|
+
_adapter = adapter;
|
|
9
|
+
}
|
|
10
|
+
function getNotificationAdapter() {
|
|
11
|
+
if (!_adapter) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"[@cyguin/notify] Notification adapter not set. Call setNotificationAdapter() before using notify()."
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
return _adapter;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/handlers/route.ts
|
|
20
|
+
function createNotifyHandler(_options) {
|
|
21
|
+
const adapter = getNotificationAdapter();
|
|
22
|
+
return {
|
|
23
|
+
async GET(request) {
|
|
24
|
+
const url = new URL(request.url);
|
|
25
|
+
const userId = url.searchParams.get("userId");
|
|
26
|
+
if (!userId) {
|
|
27
|
+
return _server.NextResponse.json({ error: "userId is required" }, { status: 400 });
|
|
28
|
+
}
|
|
29
|
+
const limit = Number(_nullishCoalesce(url.searchParams.get("limit"), () => ( 20)));
|
|
30
|
+
const offset = Number(_nullishCoalesce(url.searchParams.get("offset"), () => ( 0)));
|
|
31
|
+
const notifications = await adapter.findManyByUser(userId, { limit, offset });
|
|
32
|
+
return _server.NextResponse.json({ notifications });
|
|
33
|
+
},
|
|
34
|
+
async POST(request) {
|
|
35
|
+
let body;
|
|
36
|
+
try {
|
|
37
|
+
body = await request.json();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return _server.NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
const { userId, title, body: notificationBody, href } = body;
|
|
42
|
+
if (!userId || !title || !notificationBody) {
|
|
43
|
+
return _server.NextResponse.json({ error: "userId, title, and body are required" }, { status: 400 });
|
|
44
|
+
}
|
|
45
|
+
const options = { title, body: notificationBody, href };
|
|
46
|
+
const notification = await adapter.create({ userId, ...options });
|
|
47
|
+
return _server.NextResponse.json({ notification }, { status: 201 });
|
|
48
|
+
},
|
|
49
|
+
async PATCH(request) {
|
|
50
|
+
const url = new URL(request.url);
|
|
51
|
+
const id = url.searchParams.get("id");
|
|
52
|
+
const userId = url.searchParams.get("userId");
|
|
53
|
+
if (!id || !userId) {
|
|
54
|
+
return _server.NextResponse.json({ error: "id and userId are required" }, { status: 400 });
|
|
55
|
+
}
|
|
56
|
+
await adapter.markRead(id, userId);
|
|
57
|
+
return _server.NextResponse.json({ ok: true });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
exports.NOTIFICATION_ADAPTER = NOTIFICATION_ADAPTER; exports.setNotificationAdapter = setNotificationAdapter; exports.getNotificationAdapter = getNotificationAdapter; exports.createNotifyHandler = createNotifyHandler;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
nanoid
|
|
3
|
+
} from "./chunk-4SP667TN.js";
|
|
4
|
+
|
|
5
|
+
// src/adapters/sqlite.ts
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
var _db = null;
|
|
8
|
+
function getDb() {
|
|
9
|
+
if (!_db) {
|
|
10
|
+
_db = new Database(":memory:");
|
|
11
|
+
_db.pragma("journal_mode = WAL");
|
|
12
|
+
_db.exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
user_id TEXT NOT NULL,
|
|
16
|
+
title TEXT NOT NULL,
|
|
17
|
+
body TEXT NOT NULL,
|
|
18
|
+
href TEXT,
|
|
19
|
+
read_at INTEGER,
|
|
20
|
+
created_at INTEGER NOT NULL
|
|
21
|
+
);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at);
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
return _db;
|
|
27
|
+
}
|
|
28
|
+
function rowToRecord(row) {
|
|
29
|
+
return {
|
|
30
|
+
id: row.id,
|
|
31
|
+
userId: row.user_id,
|
|
32
|
+
title: row.title,
|
|
33
|
+
body: row.body,
|
|
34
|
+
href: row.href,
|
|
35
|
+
readAt: row.read_at,
|
|
36
|
+
createdAt: row.created_at
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
var SQLiteNotificationAdapter = {
|
|
40
|
+
async create(notification) {
|
|
41
|
+
const db = getDb();
|
|
42
|
+
const id = nanoid();
|
|
43
|
+
const createdAt = Date.now();
|
|
44
|
+
db.prepare(`
|
|
45
|
+
INSERT INTO notifications (id, user_id, title, body, href, created_at)
|
|
46
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
47
|
+
`).run(id, notification.userId, notification.title, notification.body, notification.href ?? null, createdAt);
|
|
48
|
+
return { id, ...notification, createdAt };
|
|
49
|
+
},
|
|
50
|
+
async findManyByUser(userId, { limit = 20, offset = 0 } = {}) {
|
|
51
|
+
const db = getDb();
|
|
52
|
+
const rows = db.prepare(`
|
|
53
|
+
SELECT id, user_id, title, body, href, read_at, created_at
|
|
54
|
+
FROM notifications
|
|
55
|
+
WHERE user_id = ?
|
|
56
|
+
ORDER BY created_at DESC
|
|
57
|
+
LIMIT ? OFFSET ?
|
|
58
|
+
`).all(userId, limit, offset);
|
|
59
|
+
return rows.map(rowToRecord);
|
|
60
|
+
},
|
|
61
|
+
async markRead(id, userId) {
|
|
62
|
+
const db = getDb();
|
|
63
|
+
db.prepare(`
|
|
64
|
+
UPDATE notifications SET read_at = ? WHERE id = ? AND user_id = ?
|
|
65
|
+
`).run(Date.now(), id, userId);
|
|
66
|
+
},
|
|
67
|
+
async countUnread(userId) {
|
|
68
|
+
const db = getDb();
|
|
69
|
+
const row = db.prepare(`
|
|
70
|
+
SELECT COUNT(*) as count FROM notifications WHERE user_id = ? AND read_at IS NULL
|
|
71
|
+
`).get(userId);
|
|
72
|
+
return row.count;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function createSQLiteAdapter() {
|
|
76
|
+
return SQLiteNotificationAdapter;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export {
|
|
80
|
+
SQLiteNotificationAdapter,
|
|
81
|
+
createSQLiteAdapter
|
|
82
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
nanoid
|
|
3
|
+
} from "./chunk-4SP667TN.js";
|
|
4
|
+
|
|
5
|
+
// src/adapters/postgres.ts
|
|
6
|
+
function createPostgresAdapter(connectionString) {
|
|
7
|
+
const pool = globalThis.__cyguin_postgres_pool;
|
|
8
|
+
const sql = pool ? (q, p) => pool.query(q, p).then((r) => r.rows) : async (_q, _p) => {
|
|
9
|
+
throw new Error("Postgres pool not initialized");
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
async create(notification) {
|
|
13
|
+
const id = nanoid();
|
|
14
|
+
const createdAt = Date.now();
|
|
15
|
+
await sql(
|
|
16
|
+
`INSERT INTO notifications (id, user_id, title, body, href, created_at)
|
|
17
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
18
|
+
[id, notification.userId, notification.title, notification.body, notification.href ?? null, createdAt]
|
|
19
|
+
);
|
|
20
|
+
return { id, ...notification, createdAt };
|
|
21
|
+
},
|
|
22
|
+
async findManyByUser(userId, { limit = 20, offset = 0 } = {}) {
|
|
23
|
+
const rows = await sql(
|
|
24
|
+
`SELECT id, user_id, title, body, href, read_at, created_at
|
|
25
|
+
FROM notifications
|
|
26
|
+
WHERE user_id = $1
|
|
27
|
+
ORDER BY created_at DESC
|
|
28
|
+
LIMIT $2 OFFSET $3`,
|
|
29
|
+
[userId, limit, offset]
|
|
30
|
+
);
|
|
31
|
+
return rows.map((row) => ({
|
|
32
|
+
id: row.id,
|
|
33
|
+
userId: row.user_id,
|
|
34
|
+
title: row.title,
|
|
35
|
+
body: row.body,
|
|
36
|
+
href: row.href,
|
|
37
|
+
readAt: row.read_at,
|
|
38
|
+
createdAt: row.created_at
|
|
39
|
+
}));
|
|
40
|
+
},
|
|
41
|
+
async markRead(id, userId) {
|
|
42
|
+
await sql(
|
|
43
|
+
`UPDATE notifications SET read_at = $1 WHERE id = $2 AND user_id = $3`,
|
|
44
|
+
[Date.now(), id, userId]
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
async countUnread(userId) {
|
|
48
|
+
const rows = await sql(
|
|
49
|
+
`SELECT COUNT(*) as count FROM notifications WHERE user_id = $1 AND read_at IS NULL`,
|
|
50
|
+
[userId]
|
|
51
|
+
);
|
|
52
|
+
return Number(rows[0]?.count ?? 0);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
createPostgresAdapter
|
|
59
|
+
};
|