@oimlsmart/platform-server 0.2.11 → 0.2.12
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/LICENSE +21 -0
- package/migrations/0027_notify_mentions_saved.sql +33 -0
- package/package.json +1 -1
- package/src/store/d1.ts +14 -4
- package/src/store/sqlite/events.ts +12 -7
- package/src/store/sqlite/notify.ts +7 -0
- package/src/store/sqlite/schema.sql +12 -2
- package/src/store/sqlite/store.ts +12 -0
- package/src/store.ts +23 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OIML SMART project contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
-- TODO.notify/05's remainders (platform-server#25 — the smart side's
|
|
2
|
+
-- TODO.notify/00 banner: the mentions-parse half and the per-event saved
|
|
3
|
+
-- flag ride a KERNEL migration). Expand-only per the migration contract
|
|
4
|
+
-- (append, never renumber): two columns, both NULL-defaulted, existing
|
|
5
|
+
-- rows untouched.
|
|
6
|
+
--
|
|
7
|
+
-- events.mentions the act's @user mentions (TODO.notify/00's
|
|
8
|
+
-- "Mentions land as the 'mentioned' reason"):
|
|
9
|
+
-- a JSON array of the mentioned USER IDS,
|
|
10
|
+
-- resolved by the comment surfaces' parse at
|
|
11
|
+
-- event time and recorded ON THE ROW — the
|
|
12
|
+
-- inbox feed computes its reasons at READ,
|
|
13
|
+
-- and a mention is a fact of the act, never
|
|
14
|
+
-- re-derivable from the entity. NULL = none
|
|
15
|
+
-- (every pre-0027 row, every mention-free
|
|
16
|
+
-- act). The recipient resolution's
|
|
17
|
+
-- 'mentioned' candidate class reads it; the
|
|
18
|
+
-- mutes and the visibility gate still win
|
|
19
|
+
-- (a mention never widens access).
|
|
20
|
+
--
|
|
21
|
+
-- notify_inbox_state.saved_at the per-event saved flag (TODO.notify/
|
|
22
|
+
-- 00's inbox state "read / done / saved",
|
|
23
|
+
-- the GitHub Save): stamps at the save act,
|
|
24
|
+
-- clears at the un-save. Independent of
|
|
25
|
+
-- read/done — a saved row that is done stays
|
|
26
|
+
-- saved in the archive. The marker row's
|
|
27
|
+
-- posture is unchanged (the user's own state,
|
|
28
|
+
-- lazily written, no foreign keys).
|
|
29
|
+
--
|
|
30
|
+
-- The schema's SQLite mirror (src/store/sqlite/schema.sql) updates in
|
|
31
|
+
-- the same commit; test/migrations.test.ts fails the package otherwise.
|
|
32
|
+
ALTER TABLE events ADD COLUMN mentions TEXT;
|
|
33
|
+
ALTER TABLE notify_inbox_state ADD COLUMN saved_at TEXT;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oimlsmart/platform-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "The OIML SMART platform server kernel: the store seam (ServerStore + the D1 and SQLite implementations), the canonical D1 migration set both deployments apply, the instance profile, the mailer, the RBAC map, the OIDC/OAuth client cones, and the shared role/permission vocabulary. Consumed by the smart monorepo (browser/) and the identity service.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/store/d1.ts
CHANGED
|
@@ -438,8 +438,9 @@ const INSTRUMENT_REGISTRATION_INSERT_SQL = `INSERT OR IGNORE INTO instrument_reg
|
|
|
438
438
|
|
|
439
439
|
/** The event append's INSERT … RETURNING * — ONE textual source for
|
|
440
440
|
* appendEvent and appendEvents alike (the stored row — seq + the
|
|
441
|
-
* default at — answers off the write itself).
|
|
442
|
-
|
|
441
|
+
* default at — answers off the write itself). The mentions column
|
|
442
|
+
* (migration 0027) lands from the input, NULL when absent. */
|
|
443
|
+
const EVENT_INSERT_SQL = 'INSERT INTO events (id, domain, entity_id, action, payload, mentions) VALUES (?, ?, ?, ?, ?, ?) RETURNING *'
|
|
443
444
|
|
|
444
445
|
export class D1ServerStore implements ServerStore {
|
|
445
446
|
/** The RAW binding — the ensure memos (and d1StoreFor's map) key on
|
|
@@ -3600,6 +3601,7 @@ export class D1ServerStore implements ServerStore {
|
|
|
3600
3601
|
entityId: row.entity_id as string,
|
|
3601
3602
|
action: row.action as string,
|
|
3602
3603
|
payload: row.payload as string,
|
|
3604
|
+
mentions: (row.mentions as string | null) ?? null,
|
|
3603
3605
|
at: row.at as string,
|
|
3604
3606
|
}
|
|
3605
3607
|
}
|
|
@@ -3611,7 +3613,7 @@ export class D1ServerStore implements ServerStore {
|
|
|
3611
3613
|
// two round trips per event, halved).
|
|
3612
3614
|
const row = await this.stmt(
|
|
3613
3615
|
EVENT_INSERT_SQL,
|
|
3614
|
-
input.id, input.domain, input.entityId, input.action, input.payload,
|
|
3616
|
+
input.id, input.domain, input.entityId, input.action, input.payload, input.mentions ?? null,
|
|
3615
3617
|
).first<Record<string, unknown>>()
|
|
3616
3618
|
return D1ServerStore.toPlatformEvent(row!)
|
|
3617
3619
|
}
|
|
@@ -3630,7 +3632,7 @@ export class D1ServerStore implements ServerStore {
|
|
|
3630
3632
|
const chunk = events.slice(i, i + APPEND_EVENTS_CHUNK)
|
|
3631
3633
|
const statements = chunk.map(e => this.stmt(
|
|
3632
3634
|
EVENT_INSERT_SQL,
|
|
3633
|
-
e.id, e.domain, e.entityId, e.action, e.payload,
|
|
3635
|
+
e.id, e.domain, e.entityId, e.action, e.payload, e.mentions ?? null,
|
|
3634
3636
|
))
|
|
3635
3637
|
const results = await this.db.batch(statements)
|
|
3636
3638
|
for (const res of results) {
|
|
@@ -3842,6 +3844,7 @@ export class D1ServerStore implements ServerStore {
|
|
|
3842
3844
|
eventId: row.event_id as string,
|
|
3843
3845
|
readAt: (row.read_at as string | null) ?? null,
|
|
3844
3846
|
doneAt: (row.done_at as string | null) ?? null,
|
|
3847
|
+
savedAt: (row.saved_at as string | null) ?? null,
|
|
3845
3848
|
createdAt: row.created_at as string,
|
|
3846
3849
|
}
|
|
3847
3850
|
}
|
|
@@ -3860,6 +3863,7 @@ export class D1ServerStore implements ServerStore {
|
|
|
3860
3863
|
eventId: string
|
|
3861
3864
|
read?: boolean
|
|
3862
3865
|
done?: boolean
|
|
3866
|
+
saved?: boolean
|
|
3863
3867
|
}): Promise<NotifyInboxState> {
|
|
3864
3868
|
await this.stmt(
|
|
3865
3869
|
'INSERT OR IGNORE INTO notify_inbox_state (user_id, event_id) VALUES (?, ?)',
|
|
@@ -3877,6 +3881,12 @@ export class D1ServerStore implements ServerStore {
|
|
|
3877
3881
|
input.done ? 1 : 0, input.userId, input.eventId,
|
|
3878
3882
|
).run()
|
|
3879
3883
|
}
|
|
3884
|
+
if (input.saved !== undefined) {
|
|
3885
|
+
await this.stmt(
|
|
3886
|
+
"UPDATE notify_inbox_state SET saved_at = CASE WHEN ? THEN datetime('now') ELSE NULL END WHERE user_id = ? AND event_id = ?",
|
|
3887
|
+
input.saved ? 1 : 0, input.userId, input.eventId,
|
|
3888
|
+
).run()
|
|
3889
|
+
}
|
|
3880
3890
|
const row = await this.stmt(
|
|
3881
3891
|
'SELECT * FROM notify_inbox_state WHERE user_id = ? AND event_id = ?',
|
|
3882
3892
|
input.userId, input.eventId,
|
|
@@ -18,6 +18,7 @@ interface EventRow {
|
|
|
18
18
|
entity_id: string
|
|
19
19
|
action: string
|
|
20
20
|
payload: string
|
|
21
|
+
mentions: string | null
|
|
21
22
|
at: string
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -29,17 +30,23 @@ function toPlatformEvent(row: EventRow): PlatformEvent {
|
|
|
29
30
|
entityId: row.entity_id,
|
|
30
31
|
action: row.action,
|
|
31
32
|
payload: row.payload,
|
|
33
|
+
mentions: row.mentions,
|
|
32
34
|
at: row.at,
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/** The event append's INSERT … RETURNING * — ONE textual source for
|
|
39
|
+
* appendEvent and appendEvents alike (the D1 half's EVENT_INSERT_SQL's
|
|
40
|
+
* twin; the stored row answers off the write itself). The mentions
|
|
41
|
+
* column (migration 0027) lands from the input, NULL when absent. */
|
|
42
|
+
const EVENT_INSERT_SQL = 'INSERT INTO events (id, domain, entity_id, action, payload, mentions) VALUES (?, ?, ?, ?, ?, ?) RETURNING *'
|
|
43
|
+
|
|
36
44
|
export function appendEvent(input: EventWriteInput): PlatformEvent {
|
|
37
45
|
// ONE statement: RETURNING answers the stored row (seq + the default
|
|
38
46
|
// at) off the INSERT itself — the same halving as the D1 half.
|
|
39
47
|
return toPlatformEvent(
|
|
40
|
-
getDb().prepare(
|
|
41
|
-
|
|
42
|
-
).get(input.id, input.domain, input.entityId, input.action, input.payload) as EventRow,
|
|
48
|
+
getDb().prepare(EVENT_INSERT_SQL)
|
|
49
|
+
.get(input.id, input.domain, input.entityId, input.action, input.payload, input.mentions ?? null) as EventRow,
|
|
43
50
|
)
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -55,15 +62,13 @@ export function appendEvent(input: EventWriteInput): PlatformEvent {
|
|
|
55
62
|
export function appendEvents(events: readonly EventWriteInput[]): PlatformEvent[] {
|
|
56
63
|
if (events.length === 0) return []
|
|
57
64
|
const db = getDb()
|
|
58
|
-
const insert = db.prepare(
|
|
59
|
-
`INSERT INTO events (id, domain, entity_id, action, payload) VALUES (?, ?, ?, ?, ?) RETURNING *`,
|
|
60
|
-
)
|
|
65
|
+
const insert = db.prepare(EVENT_INSERT_SQL)
|
|
61
66
|
const out: PlatformEvent[] = []
|
|
62
67
|
for (let i = 0; i < events.length; i += APPEND_EVENTS_CHUNK) {
|
|
63
68
|
const chunk = events.slice(i, i + APPEND_EVENTS_CHUNK)
|
|
64
69
|
db.transaction(() => {
|
|
65
70
|
for (const e of chunk) {
|
|
66
|
-
out.push(toPlatformEvent(insert.get(e.id, e.domain, e.entityId, e.action, e.payload) as EventRow))
|
|
71
|
+
out.push(toPlatformEvent(insert.get(e.id, e.domain, e.entityId, e.action, e.payload, e.mentions ?? null) as EventRow))
|
|
67
72
|
}
|
|
68
73
|
})()
|
|
69
74
|
}
|
|
@@ -205,6 +205,7 @@ interface InboxStateRow {
|
|
|
205
205
|
event_id: string
|
|
206
206
|
read_at: string | null
|
|
207
207
|
done_at: string | null
|
|
208
|
+
saved_at: string | null
|
|
208
209
|
created_at: string
|
|
209
210
|
}
|
|
210
211
|
|
|
@@ -214,6 +215,7 @@ function toNotifyInboxState(row: InboxStateRow): NotifyInboxState {
|
|
|
214
215
|
eventId: row.event_id,
|
|
215
216
|
readAt: row.read_at,
|
|
216
217
|
doneAt: row.done_at,
|
|
218
|
+
savedAt: row.saved_at,
|
|
217
219
|
createdAt: row.created_at,
|
|
218
220
|
}
|
|
219
221
|
}
|
|
@@ -233,6 +235,7 @@ export function putNotifyInboxState(input: {
|
|
|
233
235
|
eventId: string
|
|
234
236
|
read?: boolean
|
|
235
237
|
done?: boolean
|
|
238
|
+
saved?: boolean
|
|
236
239
|
}): NotifyInboxState {
|
|
237
240
|
const db = getDb()
|
|
238
241
|
db.prepare(
|
|
@@ -246,6 +249,10 @@ export function putNotifyInboxState(input: {
|
|
|
246
249
|
db.prepare("UPDATE notify_inbox_state SET done_at = CASE WHEN ? THEN datetime('now') ELSE NULL END WHERE user_id = ? AND event_id = ?")
|
|
247
250
|
.run(input.done ? 1 : 0, input.userId, input.eventId)
|
|
248
251
|
}
|
|
252
|
+
if (input.saved !== undefined) {
|
|
253
|
+
db.prepare("UPDATE notify_inbox_state SET saved_at = CASE WHEN ? THEN datetime('now') ELSE NULL END WHERE user_id = ? AND event_id = ?")
|
|
254
|
+
.run(input.saved ? 1 : 0, input.userId, input.eventId)
|
|
255
|
+
}
|
|
249
256
|
return toNotifyInboxState(
|
|
250
257
|
db.prepare('SELECT * FROM notify_inbox_state WHERE user_id = ? AND event_id = ?')
|
|
251
258
|
.get(input.userId, input.eventId) as InboxStateRow,
|
|
@@ -149,7 +149,12 @@ CREATE INDEX IF NOT EXISTS idx_entity_changes_store_seq ON entity_changes (store
|
|
|
149
149
|
-- envelope (the summary line, the deep link, the actors, the entity's
|
|
150
150
|
-- store for the read-time visibility gate). seq is the feed cursor;
|
|
151
151
|
-- written inside the acting request's envelope, never blocking the
|
|
152
|
-
-- triggering flow (the mailer doctrine).
|
|
152
|
+
-- triggering flow (the mailer doctrine). mentions (migration 0027,
|
|
153
|
+
-- TODO.notify/05's remainders) is the act's @user mentions as a JSON
|
|
154
|
+
-- array of user ids, resolved by the comment surfaces' parse at event
|
|
155
|
+
-- time and recorded ON THE ROW — a mention is a fact of the act, never
|
|
156
|
+
-- re-derivable from the entity at the feed's read-time computation.
|
|
157
|
+
-- NULL = none.
|
|
153
158
|
CREATE TABLE IF NOT EXISTS events (
|
|
154
159
|
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
155
160
|
id TEXT UNIQUE NOT NULL,
|
|
@@ -157,6 +162,7 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
157
162
|
entity_id TEXT NOT NULL,
|
|
158
163
|
action TEXT NOT NULL,
|
|
159
164
|
payload TEXT NOT NULL,
|
|
165
|
+
mentions TEXT,
|
|
160
166
|
at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
161
167
|
);
|
|
162
168
|
CREATE INDEX IF NOT EXISTS idx_events_domain_entity ON events (domain, entity_id);
|
|
@@ -219,7 +225,10 @@ CREATE TABLE IF NOT EXISTS notify_preferences (
|
|
|
219
225
|
-- TODO.notify/03 — the inbox state: the per-user per-event read markers
|
|
220
226
|
-- (migration 0014). One row per (user, event), written lazily at the
|
|
221
227
|
-- first act on the inbox row: read_at stamps the mark-read, done_at the
|
|
222
|
-
-- archive (a done row leaves the feed; the marker keeps the state)
|
|
228
|
+
-- archive (a done row leaves the feed; the marker keeps the state),
|
|
229
|
+
-- saved_at the per-event saved flag (migration 0027, TODO.notify/05's
|
|
230
|
+
-- remainders — the GitHub Save, independent of read/done: a saved row
|
|
231
|
+
-- that is done stays saved in the archive). The
|
|
223
232
|
-- feed itself is COMPUTED at read (events × the user's rules × the
|
|
224
233
|
-- visibility gate) — this table is the state the computation joins, and
|
|
225
234
|
-- a marker on a wiped event simply never joins (the user's state is
|
|
@@ -233,6 +242,7 @@ CREATE TABLE IF NOT EXISTS notify_inbox_state (
|
|
|
233
242
|
event_id TEXT NOT NULL,
|
|
234
243
|
read_at TEXT,
|
|
235
244
|
done_at TEXT,
|
|
245
|
+
saved_at TEXT,
|
|
236
246
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
237
247
|
PRIMARY KEY (user_id, event_id)
|
|
238
248
|
);
|
|
@@ -154,6 +154,18 @@ function migrateAuthTables(db: Database.Database): void {
|
|
|
154
154
|
if (emailChangeCols.length && !emailChangeCols.some(c => c.name === 'kind')) {
|
|
155
155
|
db.exec("ALTER TABLE email_change_tokens ADD COLUMN kind TEXT NOT NULL DEFAULT 'change'")
|
|
156
156
|
}
|
|
157
|
+
// TODO.notify/05's remainders (migration 0027): the event row's
|
|
158
|
+
// mentions column + the inbox marker's saved_at stamp — a dev file
|
|
159
|
+
// predating it grows the columns here. NULL = none / unsaved: existing
|
|
160
|
+
// rows keep their posture silently.
|
|
161
|
+
const eventCols = db.prepare('PRAGMA table_info(events)').all() as Array<{ name: string }>
|
|
162
|
+
if (eventCols.length && !eventCols.some(c => c.name === 'mentions')) {
|
|
163
|
+
db.exec('ALTER TABLE events ADD COLUMN mentions TEXT')
|
|
164
|
+
}
|
|
165
|
+
const inboxCols = db.prepare('PRAGMA table_info(notify_inbox_state)').all() as Array<{ name: string }>
|
|
166
|
+
if (inboxCols.length && !inboxCols.some(c => c.name === 'saved_at')) {
|
|
167
|
+
db.exec('ALTER TABLE notify_inbox_state ADD COLUMN saved_at TEXT')
|
|
168
|
+
}
|
|
157
169
|
}
|
|
158
170
|
|
|
159
171
|
// AuthUserPayload lives in ./backend (see the re-export above).
|
package/src/store.ts
CHANGED
|
@@ -206,7 +206,12 @@ export const PUT_ENTITIES_CHUNK = 50
|
|
|
206
206
|
* composed key is derived at read, never stored. `payload` is the JSON
|
|
207
207
|
* envelope the catalog row declares (the summary line, the deep link,
|
|
208
208
|
* the actors, the entity's store for the read-time visibility gate).
|
|
209
|
-
* `
|
|
209
|
+
* `mentions` (migration 0027, TODO.notify/05's remainders) is the act's
|
|
210
|
+
* @user mentions as a JSON array of user ids — the comment surfaces'
|
|
211
|
+
* parse resolves them at event time and records them ON THE ROW (a
|
|
212
|
+
* mention is a fact of the act; the feed's read-time reason computation
|
|
213
|
+
* could never re-derive it from the entity). NULL = none. `seq` is the
|
|
214
|
+
* feed cursor (the entity_changes journal's pattern). */
|
|
210
215
|
export interface PlatformEvent {
|
|
211
216
|
seq: number
|
|
212
217
|
id: string
|
|
@@ -214,6 +219,9 @@ export interface PlatformEvent {
|
|
|
214
219
|
entityId: string
|
|
215
220
|
action: string
|
|
216
221
|
payload: string
|
|
222
|
+
/** The mentioned user ids' JSON array (raw, the payload/channelOverrides
|
|
223
|
+
* posture — the consumer parses); NULL = no mentions. */
|
|
224
|
+
mentions: string | null
|
|
217
225
|
at: string
|
|
218
226
|
}
|
|
219
227
|
|
|
@@ -226,6 +234,9 @@ export interface EventWriteInput {
|
|
|
226
234
|
entityId: string
|
|
227
235
|
action: string
|
|
228
236
|
payload: string
|
|
237
|
+
/** The act's mentioned user ids (JSON array; TODO.notify/05's
|
|
238
|
+
* remainders) — absent/NULL lands NULL (no mentions). */
|
|
239
|
+
mentions?: string | null
|
|
229
240
|
}
|
|
230
241
|
|
|
231
242
|
/** The key pattern's SQL legs: a subscription pattern (`application/**`,
|
|
@@ -330,7 +341,10 @@ export interface NotifyPreferences {
|
|
|
330
341
|
* lazily at read"): one row per (user, event), created at the first act
|
|
331
342
|
* on the inbox row. `readAt` stamps the mark-read (NULL = unread);
|
|
332
343
|
* `doneAt` stamps the archive (NULL = in the inbox — a done row leaves
|
|
333
|
-
* the feed, the marker keeps the state)
|
|
344
|
+
* the feed, the marker keeps the state); `savedAt` (migration 0027,
|
|
345
|
+
* TODO.notify/05's remainders) stamps the per-event saved flag — the
|
|
346
|
+
* GitHub Save, independent of read/done (a saved row that is done stays
|
|
347
|
+
* saved in the archive).
|
|
334
348
|
* The feed is COMPUTED at read (events × the user's rules × the
|
|
335
349
|
* visibility gate); this table is the state that computation joins. A
|
|
336
350
|
* marker on a wiped event never joins (the user's state is their own,
|
|
@@ -340,6 +354,7 @@ export interface NotifyInboxState {
|
|
|
340
354
|
eventId: string
|
|
341
355
|
readAt: string | null
|
|
342
356
|
doneAt: string | null
|
|
357
|
+
savedAt: string | null
|
|
343
358
|
createdAt: string
|
|
344
359
|
}
|
|
345
360
|
|
|
@@ -2593,17 +2608,20 @@ export interface ServerStore {
|
|
|
2593
2608
|
putNotifyPreferences(userId: string, channels: string): Promise<NotifyPreferences>
|
|
2594
2609
|
|
|
2595
2610
|
// ── the inbox state (TODO.notify/03) ──
|
|
2596
|
-
/** The user's inbox markers (the feed's join: read/done per
|
|
2597
|
-
* Written lazily at the first act — most events carry no row. */
|
|
2611
|
+
/** The user's inbox markers (the feed's join: read/done/saved per
|
|
2612
|
+
* event). Written lazily at the first act — most events carry no row. */
|
|
2598
2613
|
listNotifyInboxStates(userId: string): Promise<NotifyInboxState[]>
|
|
2599
2614
|
/** The marker write (the upsert on PRIMARY KEY (user_id, event_id)):
|
|
2600
2615
|
* each PRESENT flag sets its stamp (datetime('now')) or clears it
|
|
2601
|
-
* (NULL); absent flags keep.
|
|
2616
|
+
* (NULL); absent flags keep. `saved` (migration 0027, TODO.notify/05's
|
|
2617
|
+
* remainders) is the per-event saved flag, the same stamp/clear
|
|
2618
|
+
* posture. Answers the stored row. */
|
|
2602
2619
|
putNotifyInboxState(input: {
|
|
2603
2620
|
userId: string
|
|
2604
2621
|
eventId: string
|
|
2605
2622
|
read?: boolean
|
|
2606
2623
|
done?: boolean
|
|
2624
|
+
saved?: boolean
|
|
2607
2625
|
}): Promise<NotifyInboxState>
|
|
2608
2626
|
|
|
2609
2627
|
// ── the email channel's delivery store (TODO.notify/04) ──
|