@warlock.js/notifications 4.2.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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/cjs/index.cjs +1034 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/channels/database-channel.d.mts +10 -0
- package/esm/channels/database-channel.d.mts.map +1 -0
- package/esm/channels/database-channel.mjs +19 -0
- package/esm/channels/database-channel.mjs.map +1 -0
- package/esm/channels/index.d.mts +2 -0
- package/esm/channels/index.mjs +4 -0
- package/esm/channels/mail-channel.d.mts +11 -0
- package/esm/channels/mail-channel.d.mts.map +1 -0
- package/esm/channels/mail-channel.mjs +43 -0
- package/esm/channels/mail-channel.mjs.map +1 -0
- package/esm/config.d.mts +46 -0
- package/esm/config.d.mts.map +1 -0
- package/esm/config.mjs +32 -0
- package/esm/config.mjs.map +1 -0
- package/esm/contracts/channel.contract.d.mts +38 -0
- package/esm/contracts/channel.contract.d.mts.map +1 -0
- package/esm/contracts/index.d.mts +5 -0
- package/esm/contracts/notification.contract.d.mts +18 -0
- package/esm/contracts/notification.contract.d.mts.map +1 -0
- package/esm/contracts/preference-provider.contract.d.mts +20 -0
- package/esm/contracts/preference-provider.contract.d.mts.map +1 -0
- package/esm/contracts/queue-dispatcher.contract.d.mts +23 -0
- package/esm/contracts/queue-dispatcher.contract.d.mts.map +1 -0
- package/esm/contracts/rate-limiter.contract.d.mts +23 -0
- package/esm/contracts/rate-limiter.contract.d.mts.map +1 -0
- package/esm/dispatch/define-channel.d.mts +37 -0
- package/esm/dispatch/define-channel.d.mts.map +1 -0
- package/esm/dispatch/define-channel.mjs +39 -0
- package/esm/dispatch/define-channel.mjs.map +1 -0
- package/esm/dispatch/define-notification.d.mts +22 -0
- package/esm/dispatch/define-notification.d.mts.map +1 -0
- package/esm/dispatch/define-notification.mjs +125 -0
- package/esm/dispatch/define-notification.mjs.map +1 -0
- package/esm/dispatch/dispatch-id.mjs +15 -0
- package/esm/dispatch/dispatch-id.mjs.map +1 -0
- package/esm/dispatch/dispatcher.mjs +120 -0
- package/esm/dispatch/dispatcher.mjs.map +1 -0
- package/esm/dispatch/index.d.mts +4 -0
- package/esm/dispatch/index.mjs +6 -0
- package/esm/dispatch/notifications-event-bus.d.mts +18 -0
- package/esm/dispatch/notifications-event-bus.d.mts.map +1 -0
- package/esm/dispatch/notifications-event-bus.mjs +58 -0
- package/esm/dispatch/notifications-event-bus.mjs.map +1 -0
- package/esm/dispatch/notify.d.mts +20 -0
- package/esm/dispatch/notify.d.mts.map +1 -0
- package/esm/dispatch/notify.mjs +80 -0
- package/esm/dispatch/notify.mjs.map +1 -0
- package/esm/errors.d.mts +32 -0
- package/esm/errors.d.mts.map +1 -0
- package/esm/errors.mjs +56 -0
- package/esm/errors.mjs.map +1 -0
- package/esm/in-app/base-notifications-repository.d.mts +82 -0
- package/esm/in-app/base-notifications-repository.d.mts.map +1 -0
- package/esm/in-app/base-notifications-repository.mjs +187 -0
- package/esm/in-app/base-notifications-repository.mjs.map +1 -0
- package/esm/in-app/column-map.d.mts +65 -0
- package/esm/in-app/column-map.d.mts.map +1 -0
- package/esm/in-app/column-map.mjs +28 -0
- package/esm/in-app/column-map.mjs.map +1 -0
- package/esm/in-app/database-notification.d.mts +30 -0
- package/esm/in-app/database-notification.d.mts.map +1 -0
- package/esm/in-app/database-notification.mjs +70 -0
- package/esm/in-app/database-notification.mjs.map +1 -0
- package/esm/in-app/in-app.d.mts +56 -0
- package/esm/in-app/in-app.d.mts.map +1 -0
- package/esm/in-app/in-app.mjs +67 -0
- package/esm/in-app/in-app.mjs.map +1 -0
- package/esm/in-app/index.d.mts +4 -0
- package/esm/in-app/index.mjs +6 -0
- package/esm/index.d.mts +25 -0
- package/esm/index.mjs +23 -0
- package/esm/migration/index.d.mts +1 -0
- package/esm/migration/index.mjs +3 -0
- package/esm/migration/notification-columns.d.mts +13 -0
- package/esm/migration/notification-columns.d.mts.map +1 -0
- package/esm/migration/notification-columns.mjs +53 -0
- package/esm/migration/notification-columns.mjs.map +1 -0
- package/esm/queue/herald-queue.d.mts +10 -0
- package/esm/queue/herald-queue.d.mts.map +1 -0
- package/esm/queue/herald-queue.mjs +14 -0
- package/esm/queue/herald-queue.mjs.map +1 -0
- package/esm/queue/index.d.mts +3 -0
- package/esm/queue/index.mjs +5 -0
- package/esm/queue/load-herald.d.mts +6 -0
- package/esm/queue/load-herald.d.mts.map +1 -0
- package/esm/queue/load-herald.mjs +35 -0
- package/esm/queue/load-herald.mjs.map +1 -0
- package/esm/queue/notifications-worker.d.mts +9 -0
- package/esm/queue/notifications-worker.d.mts.map +1 -0
- package/esm/queue/notifications-worker.mjs +46 -0
- package/esm/queue/notifications-worker.mjs.map +1 -0
- package/esm/types.d.mts +130 -0
- package/esm/types.d.mts.map +1 -0
- package/llms-full.txt +994 -0
- package/llms.txt +17 -0
- package/package.json +40 -0
- package/skills/README.md +23 -0
- package/skills/configure-notifications/SKILL.md +138 -0
- package/skills/define-channel/SKILL.md +107 -0
- package/skills/define-notification/SKILL.md +133 -0
- package/skills/notifications-basics/SKILL.md +94 -0
- package/skills/observe-notifications/SKILL.md +66 -0
- package/skills/queue-notifications/SKILL.md +81 -0
- package/skills/send-ad-hoc/SKILL.md +95 -0
- package/skills/use-in-app/SKILL.md +168 -0
- package/skills/write-notification-migration/SKILL.md +70 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../@warlock.js/notifications/src/errors.ts"],"mappings":";;AAiBA;;;;;AAWA;;cAXa,+BAAA,SAAwC,KAAK;EAAL,WAAA;AAAA;AAAA,cAWxC,oBAAA,SAA6B,KAAK;cAC1B,WAAA;AAAA;AAAA,cAUR,sBAAA,SAA+B,KAAK;cAC5B,WAAA;AAAA;AAAA,cAUR,oBAAA,SAA6B,KAAK;cAC1B,gBAAA,UAA0B,WAAA;AAAA;;;;AAXP;AAUxC;cAgBa,sBAAA,SAA+B,KAAK;cAC5B,WAAA,UAAqB,WAAA;AAAA"}
|
package/esm/errors.mjs
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//#region ../@warlock.js/notifications/src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Domain errors for `@warlock.js/notifications`. All extend `Error` directly
|
|
4
|
+
* (no framework HttpError) so the package stays runtime-agnostic — the host
|
|
5
|
+
* app maps them to its own error shape via `instanceof` if needed.
|
|
6
|
+
*
|
|
7
|
+
* Each captures a clean V8 stack (drops the Error-constructor frame) to match
|
|
8
|
+
* the cascade convention.
|
|
9
|
+
*/
|
|
10
|
+
function captureStack(target, ctor) {
|
|
11
|
+
Error.captureStackTrace?.(target, ctor);
|
|
12
|
+
}
|
|
13
|
+
var NotificationsNotConfiguredError = class NotificationsNotConfiguredError extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super("Notifications not configured — add `src/config/notifications.ts` with a default-exported config; the notifications connector registers it at boot.");
|
|
16
|
+
this.name = "NotificationsNotConfiguredError";
|
|
17
|
+
captureStack(this, NotificationsNotConfiguredError);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var ChannelNotFoundError = class ChannelNotFoundError extends Error {
|
|
21
|
+
constructor(channelName) {
|
|
22
|
+
super(`Channel "${channelName}" is not configured. Add it to the \`channels\` map in \`src/config/notifications.ts\`.`);
|
|
23
|
+
this.name = "ChannelNotFoundError";
|
|
24
|
+
captureStack(this, ChannelNotFoundError);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var NoQueueDispatcherError = class NoQueueDispatcherError extends Error {
|
|
28
|
+
constructor(channelName) {
|
|
29
|
+
super(`Cannot queue to channel "${channelName}" — no queue dispatcher configured. Add a \`queue\` to \`src/config/notifications.ts\`, or call \`.send()\` instead of \`.queue()\`.`);
|
|
30
|
+
this.name = "NoQueueDispatcherError";
|
|
31
|
+
captureStack(this, NoQueueDispatcherError);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var MissingRendererError = class MissingRendererError extends Error {
|
|
35
|
+
constructor(notificationType, channelName) {
|
|
36
|
+
super(`Notification "${notificationType}" routes to channel "${channelName}" via \`via\` but defines no \`${channelName}\` renderer.`);
|
|
37
|
+
this.name = "MissingRendererError";
|
|
38
|
+
captureStack(this, MissingRendererError);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Thrown when a channel's `route()` resolver runs but returns `undefined` for
|
|
43
|
+
* a recipient (e.g. a mail recipient with no email). Distinct from "the
|
|
44
|
+
* channel declares no resolver" — that case falls back to `{ id }`.
|
|
45
|
+
*/
|
|
46
|
+
var UnresolvableRouteError = class UnresolvableRouteError extends Error {
|
|
47
|
+
constructor(channelName, recipientId) {
|
|
48
|
+
super(`Channel "${channelName}" could not resolve a route for recipient ${recipientId ?? "(unknown)"}. The channel's \`route()\` returned undefined — the recipient is missing the address column this channel needs.`);
|
|
49
|
+
this.name = "UnresolvableRouteError";
|
|
50
|
+
captureStack(this, UnresolvableRouteError);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { ChannelNotFoundError, MissingRendererError, NoQueueDispatcherError, NotificationsNotConfiguredError, UnresolvableRouteError };
|
|
56
|
+
//# sourceMappingURL=errors.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../@warlock.js/notifications/src/errors.ts"],"sourcesContent":["/**\n * Domain errors for `@warlock.js/notifications`. All extend `Error` directly\n * (no framework HttpError) so the package stays runtime-agnostic — the host\n * app maps them to its own error shape via `instanceof` if needed.\n *\n * Each captures a clean V8 stack (drops the Error-constructor frame) to match\n * the cascade convention.\n */\n\nfunction captureStack(target: object, ctor: new (...args: any[]) => unknown): void {\n // V8 only; guarded for non-V8 runtimes.\n (Error as unknown as { captureStackTrace?: (t: object, c: unknown) => void }).captureStackTrace?.(\n target,\n ctor,\n );\n}\n\nexport class NotificationsNotConfiguredError extends Error {\n public constructor() {\n super(\n \"Notifications not configured — add `src/config/notifications.ts` with a \" +\n \"default-exported config; the notifications connector registers it at boot.\",\n );\n this.name = \"NotificationsNotConfiguredError\";\n captureStack(this, NotificationsNotConfiguredError);\n }\n}\n\nexport class ChannelNotFoundError extends Error {\n public constructor(channelName: string) {\n super(\n `Channel \"${channelName}\" is not configured. Add it to the ` +\n \"`channels` map in `src/config/notifications.ts`.\",\n );\n this.name = \"ChannelNotFoundError\";\n captureStack(this, ChannelNotFoundError);\n }\n}\n\nexport class NoQueueDispatcherError extends Error {\n public constructor(channelName: string) {\n super(\n `Cannot queue to channel \"${channelName}\" — no queue dispatcher configured. ` +\n \"Add a `queue` to `src/config/notifications.ts`, or call `.send()` instead of `.queue()`.\",\n );\n this.name = \"NoQueueDispatcherError\";\n captureStack(this, NoQueueDispatcherError);\n }\n}\n\nexport class MissingRendererError extends Error {\n public constructor(notificationType: string, channelName: string) {\n super(\n `Notification \"${notificationType}\" routes to channel \"${channelName}\" via ` +\n `\\`via\\` but defines no \\`${channelName}\\` renderer.`,\n );\n this.name = \"MissingRendererError\";\n captureStack(this, MissingRendererError);\n }\n}\n\n/**\n * Thrown when a channel's `route()` resolver runs but returns `undefined` for\n * a recipient (e.g. a mail recipient with no email). Distinct from \"the\n * channel declares no resolver\" — that case falls back to `{ id }`.\n */\nexport class UnresolvableRouteError extends Error {\n public constructor(channelName: string, recipientId?: unknown) {\n super(\n `Channel \"${channelName}\" could not resolve a route for recipient ` +\n `${recipientId ?? \"(unknown)\"}. The channel's \\`route()\\` returned undefined — ` +\n \"the recipient is missing the address column this channel needs.\",\n );\n this.name = \"UnresolvableRouteError\";\n captureStack(this, UnresolvableRouteError);\n }\n}\n"],"mappings":";;;;;;;;;AASA,SAAS,aAAa,QAAgB,MAA6C;CAEjF,AAAC,MAA6E,oBAC5E,QACA,IACF;AACF;AAEA,IAAa,kCAAb,MAAa,wCAAwC,MAAM;CACzD,AAAO,cAAc;EACnB,MACE,oJAEF;EACA,KAAK,OAAO;EACZ,aAAa,MAAM,+BAA+B;CACpD;AACF;AAEA,IAAa,uBAAb,MAAa,6BAA6B,MAAM;CAC9C,AAAO,YAAY,aAAqB;EACtC,MACE,YAAY,YAAY,wFAE1B;EACA,KAAK,OAAO;EACZ,aAAa,MAAM,oBAAoB;CACzC;AACF;AAEA,IAAa,yBAAb,MAAa,+BAA+B,MAAM;CAChD,AAAO,YAAY,aAAqB;EACtC,MACE,4BAA4B,YAAY,qIAE1C;EACA,KAAK,OAAO;EACZ,aAAa,MAAM,sBAAsB;CAC3C;AACF;AAEA,IAAa,uBAAb,MAAa,6BAA6B,MAAM;CAC9C,AAAO,YAAY,kBAA0B,aAAqB;EAChE,MACE,iBAAiB,iBAAiB,uBAAuB,YAAY,iCACvC,YAAY,aAC5C;EACA,KAAK,OAAO;EACZ,aAAa,MAAM,oBAAoB;CACzC;AACF;;;;;;AAOA,IAAa,yBAAb,MAAa,+BAA+B,MAAM;CAChD,AAAO,YAAY,aAAqB,aAAuB;EAC7D,MACE,YAAY,YAAY,4CACnB,eAAe,YAAY,iHAElC;EACA,KAAK,OAAO;EACZ,aAAa,MAAM,sBAAsB;CAC3C;AACF"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { DatabasePayload, Id } from "../types.mjs";
|
|
2
|
+
import { NotificationContract } from "../contracts/notification.contract.mjs";
|
|
3
|
+
import { NotificationColumnMapHost } from "./column-map.mjs";
|
|
4
|
+
import { DatabaseNotification } from "./database-notification.mjs";
|
|
5
|
+
import { FilterRules, RepositoryManager } from "@warlock.js/core";
|
|
6
|
+
|
|
7
|
+
//#region ../@warlock.js/notifications/src/in-app/base-notifications-repository.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Filter shape — logical keys the repo understands. `filterBy` maps each to a
|
|
10
|
+
* physical column (or a predicate) derived from the model's `columnMap`.
|
|
11
|
+
*/
|
|
12
|
+
type NotificationsFilter = {
|
|
13
|
+
id?: Id;
|
|
14
|
+
type?: string; /** Mode-agnostic unread filter — resolves to the model's read-state column. */
|
|
15
|
+
unread?: boolean;
|
|
16
|
+
recipientId?: Id;
|
|
17
|
+
idempotencyKey?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Class form of a `DatabaseNotification` subclass — what `configure()` accepts. */
|
|
20
|
+
type NotificationModelClass = (new (...args: any[]) => DatabaseNotification) & NotificationColumnMapHost;
|
|
21
|
+
declare class BaseNotificationsRepository<TModel extends NotificationContract = NotificationContract> extends RepositoryManager<TModel, NotificationsFilter> {
|
|
22
|
+
/** Resolved physical columns for the bound model — the single source. */
|
|
23
|
+
private readonly columns;
|
|
24
|
+
filterBy: FilterRules;
|
|
25
|
+
constructor(model?: NotificationModelClass);
|
|
26
|
+
/** The tenant column for this model, or `undefined` for single-tenant. */
|
|
27
|
+
get tenantColumn(): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Build `filterBy` from the resolved column map. `unread` is the
|
|
30
|
+
* mode-agnostic read filter; `isRead` / `readAt` / `tenant` are exposed for
|
|
31
|
+
* direct filtering only when the model declares those columns.
|
|
32
|
+
*/
|
|
33
|
+
private buildFilterBy;
|
|
34
|
+
/**
|
|
35
|
+
* Translate an object keyed by LOGICAL names into one keyed by PHYSICAL
|
|
36
|
+
* columns, using `filterBy` as the lookup. Keys with no `[op, column]` tuple
|
|
37
|
+
* (e.g. `title`, `body`, `payload`, `type`, `id`) pass through unchanged —
|
|
38
|
+
* they're already physical. Used for every write / update / delete payload +
|
|
39
|
+
* filter (the paths `RepositoryManager` does NOT map).
|
|
40
|
+
*/
|
|
41
|
+
protected toRow(obj: Record<string, unknown>): Record<string, unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* Create one row for the recipient. New rows start unread; the tenant column
|
|
44
|
+
* (when the model declares one) is written from `tenantId`, which the
|
|
45
|
+
* database channel reads off the recipient.
|
|
46
|
+
*
|
|
47
|
+
* Idempotency: when `input.idempotencyKey` is set, find-or-create — return
|
|
48
|
+
* the existing row instead of inserting a duplicate. A unique index on the
|
|
49
|
+
* key is the backstop for the rare insert race (we re-fetch on conflict).
|
|
50
|
+
*/
|
|
51
|
+
createFor(recipientId: Id, input: DatabasePayload, tenantId?: Id): Promise<TModel>;
|
|
52
|
+
/**
|
|
53
|
+
* Bulk-create one row per recipient sharing the same input + tenant. Reserved
|
|
54
|
+
* for the Phase-2 `Channel.sendMany` fan-out hook; not on the per-recipient
|
|
55
|
+
* send path yet.
|
|
56
|
+
*/
|
|
57
|
+
createManyFor(recipientIds: Id[], input: DatabasePayload, tenantId?: Id): Promise<TModel[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Mark rows read — recipient-scoped. `id` omitted → all unread rows for the
|
|
60
|
+
* recipient; `id` given → that one row, still scoped to the recipient. Scoped
|
|
61
|
+
* to unread so a re-mark never overwrites an earlier `read_at` timestamp.
|
|
62
|
+
* Sets whichever read-state column(s) the model declares.
|
|
63
|
+
*/
|
|
64
|
+
markRead(recipientId: Id, id?: Id): Promise<number>;
|
|
65
|
+
/**
|
|
66
|
+
* Inverse of `markRead`, same recipient-scoping. No unread scope needed —
|
|
67
|
+
* clearing the read-state of an already-unread row is a no-op.
|
|
68
|
+
*/
|
|
69
|
+
markUnread(recipientId: Id, id?: Id): Promise<number>;
|
|
70
|
+
/** Find one row for a recipient (read path — `filterBy` maps the keys). */
|
|
71
|
+
findFor(recipientId: Id, id: Id): Promise<TModel | null>;
|
|
72
|
+
/**
|
|
73
|
+
* Delete rows — recipient-scoped. `id` omitted → clear all for the
|
|
74
|
+
* recipient; `id` given → that one row.
|
|
75
|
+
*/
|
|
76
|
+
deleteFor(recipientId: Id, id?: Id): Promise<number>;
|
|
77
|
+
/** Read path — `filterBy` maps `recipientId`/`idempotencyKey` to columns. */
|
|
78
|
+
private firstByIdempotencyKey;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { BaseNotificationsRepository, NotificationModelClass, NotificationsFilter };
|
|
82
|
+
//# sourceMappingURL=base-notifications-repository.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-notifications-repository.d.mts","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/in-app/base-notifications-repository.ts"],"mappings":";;;;;;;;;;;KA2CY,mBAAA;EACV,EAAA,GAAK,EAAA;EACL,IAAA,WA0Ge;EAxGf,MAAA;EACA,WAAA,GAAc,EAAE;EAChB,cAAA;AAAA;;KAIU,sBAAA,YAAkC,IAAA,YAAgB,oBAAA,IAC5D,yBAAyB;AAAA,cAEd,2BAAA,gBACI,oBAAA,GAAuB,oBAAA,UAC9B,iBAAA,CAAkB,MAAA,EAAQ,mBAAA;EA4IvB;EAAA,iBA1IM,OAAA;EAEV,QAAA,EAAU,WAAA;cAEE,KAAA,GAAQ,sBAAA;EAkJgB;EAAA,IAtIhC,YAAA;EA8J6B;;;;;EAAA,QArJhC,aAAA;EAgLsB;;;;;;;EAAA,UArIpB,KAAA,CAAM,GAAA,EAAK,MAAA,oBAA0B,MAAA;EAvET;;;;;;;;;EA6FzB,SAAA,CACX,WAAA,EAAa,EAAA,EACb,KAAA,EAAO,eAAA,EACP,QAAA,GAAW,EAAA,GACV,OAAA,CAAQ,MAAA;EA9EA;;;;;EAsHJ,aAAA,CACL,YAAA,EAAc,EAAA,IACd,KAAA,EAAO,eAAA,EACP,QAAA,GAAW,EAAA,GACV,OAAA,CAAQ,MAAA;EAhDE;;;;;;EA4DN,QAAA,CAAS,WAAA,EAAa,EAAA,EAAI,EAAA,GAAK,EAAA,GAAK,OAAA;EAxDxC;;;;EAgFI,UAAA,CAAW,WAAA,EAAa,EAAA,EAAI,EAAA,GAAK,EAAA,GAAK,OAAA;EAtCpC;EAyDF,OAAA,CAAQ,WAAA,EAAa,EAAA,EAAI,EAAA,EAAI,EAAA,GAAK,OAAA,CAAQ,MAAA;EAxDpC;;;;EAgEN,SAAA,CAAU,WAAA,EAAa,EAAA,EAAI,EAAA,GAAK,EAAA,GAAK,OAAA;EAnDf;EAAA,QAwDrB,qBAAA;AAAA"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { resolveColumnMap } from "./column-map.mjs";
|
|
2
|
+
import { RepositoryManager } from "@warlock.js/core";
|
|
3
|
+
|
|
4
|
+
//#region ../@warlock.js/notifications/src/in-app/base-notifications-repository.ts
|
|
5
|
+
/**
|
|
6
|
+
* Shipped behavior repository for the in-app store.
|
|
7
|
+
*
|
|
8
|
+
* Concrete (NOT abstract) so `inApp.configure({ model })` can
|
|
9
|
+
* `new BaseNotificationsRepository(model)` without subclassing. The dev only
|
|
10
|
+
* extends this when they need extra query methods.
|
|
11
|
+
*
|
|
12
|
+
* ## The single source of truth: the model's `columnMap`
|
|
13
|
+
*
|
|
14
|
+
* The model declares its physical columns ONCE via `static columnMap`. The
|
|
15
|
+
* constructor resolves that map and derives BOTH sides from it:
|
|
16
|
+
*
|
|
17
|
+
* - the READ path — `filterBy` maps logical keys (`recipientId`, `unread`,
|
|
18
|
+
* `type`, …) onto the model's columns; `RepositoryManager` applies it.
|
|
19
|
+
* - the WRITE / UPDATE / DELETE path — `createFor` / `markRead` / … translate
|
|
20
|
+
* logical keys through the same `filterBy` via `toRow`.
|
|
21
|
+
*
|
|
22
|
+
* Because both come from one `columnMap`, reads and writes can never target
|
|
23
|
+
* different columns. `unread` is mode-agnostic: it filters `is_read = false`
|
|
24
|
+
* when the model has that flag, otherwise `read_at IS NULL`.
|
|
25
|
+
*
|
|
26
|
+
* @example default (SQL snake_case — no subclass needed)
|
|
27
|
+
* inApp.configure({ model: Notification })
|
|
28
|
+
*
|
|
29
|
+
* @example custom columns — declare them on the model, not here
|
|
30
|
+
* class Notification extends DatabaseNotification {
|
|
31
|
+
* public static columnMap = { recipient: "audience_id", isRead: "seen" };
|
|
32
|
+
* }
|
|
33
|
+
*/
|
|
34
|
+
var BaseNotificationsRepository = class extends RepositoryManager {
|
|
35
|
+
/** Resolved physical columns for the bound model — the single source. */
|
|
36
|
+
columns;
|
|
37
|
+
filterBy;
|
|
38
|
+
constructor(model) {
|
|
39
|
+
super();
|
|
40
|
+
if (model) this.source = model;
|
|
41
|
+
this.columns = resolveColumnMap(model?.columnMap);
|
|
42
|
+
this.filterBy = this.buildFilterBy();
|
|
43
|
+
}
|
|
44
|
+
/** The tenant column for this model, or `undefined` for single-tenant. */
|
|
45
|
+
get tenantColumn() {
|
|
46
|
+
return this.columns.tenant;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build `filterBy` from the resolved column map. `unread` is the
|
|
50
|
+
* mode-agnostic read filter; `isRead` / `readAt` / `tenant` are exposed for
|
|
51
|
+
* direct filtering only when the model declares those columns.
|
|
52
|
+
*/
|
|
53
|
+
buildFilterBy() {
|
|
54
|
+
const { recipient, tenant, readAt, isRead } = this.columns;
|
|
55
|
+
const rules = {
|
|
56
|
+
id: "=",
|
|
57
|
+
type: "=",
|
|
58
|
+
recipientId: ["=", recipient],
|
|
59
|
+
idempotencyKey: ["=", "idempotency_key"],
|
|
60
|
+
unread: (value, query) => {
|
|
61
|
+
if (value !== true && value !== "true") return;
|
|
62
|
+
if (isRead) query.where(isRead, false);
|
|
63
|
+
else if (readAt) query.whereNull(readAt);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
if (isRead) rules.isRead = ["=", isRead];
|
|
67
|
+
if (readAt) rules.readAt = ["=", readAt];
|
|
68
|
+
if (tenant) rules.tenant = ["=", tenant];
|
|
69
|
+
return rules;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Translate an object keyed by LOGICAL names into one keyed by PHYSICAL
|
|
73
|
+
* columns, using `filterBy` as the lookup. Keys with no `[op, column]` tuple
|
|
74
|
+
* (e.g. `title`, `body`, `payload`, `type`, `id`) pass through unchanged —
|
|
75
|
+
* they're already physical. Used for every write / update / delete payload +
|
|
76
|
+
* filter (the paths `RepositoryManager` does NOT map).
|
|
77
|
+
*/
|
|
78
|
+
toRow(obj) {
|
|
79
|
+
const rules = this.filterBy;
|
|
80
|
+
const row = {};
|
|
81
|
+
for (const [logical, value] of Object.entries(obj)) {
|
|
82
|
+
const rule = rules[logical];
|
|
83
|
+
const column = Array.isArray(rule) ? rule[1] : logical;
|
|
84
|
+
row[column] = value;
|
|
85
|
+
}
|
|
86
|
+
return row;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create one row for the recipient. New rows start unread; the tenant column
|
|
90
|
+
* (when the model declares one) is written from `tenantId`, which the
|
|
91
|
+
* database channel reads off the recipient.
|
|
92
|
+
*
|
|
93
|
+
* Idempotency: when `input.idempotencyKey` is set, find-or-create — return
|
|
94
|
+
* the existing row instead of inserting a duplicate. A unique index on the
|
|
95
|
+
* key is the backstop for the rare insert race (we re-fetch on conflict).
|
|
96
|
+
*/
|
|
97
|
+
async createFor(recipientId, input, tenantId) {
|
|
98
|
+
const { idempotencyKey } = input;
|
|
99
|
+
if (idempotencyKey) {
|
|
100
|
+
const existing = await this.firstByIdempotencyKey(recipientId, idempotencyKey);
|
|
101
|
+
if (existing) return existing;
|
|
102
|
+
}
|
|
103
|
+
const row = {
|
|
104
|
+
recipientId,
|
|
105
|
+
...input
|
|
106
|
+
};
|
|
107
|
+
if (this.columns.isRead) row.isRead = false;
|
|
108
|
+
if (this.columns.tenant && tenantId !== void 0) row.tenant = tenantId;
|
|
109
|
+
try {
|
|
110
|
+
return await this.create(this.toRow(row));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (idempotencyKey) {
|
|
113
|
+
const existing = await this.firstByIdempotencyKey(recipientId, idempotencyKey);
|
|
114
|
+
if (existing) return existing;
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Bulk-create one row per recipient sharing the same input + tenant. Reserved
|
|
121
|
+
* for the Phase-2 `Channel.sendMany` fan-out hook; not on the per-recipient
|
|
122
|
+
* send path yet.
|
|
123
|
+
*/
|
|
124
|
+
createManyFor(recipientIds, input, tenantId) {
|
|
125
|
+
return Promise.all(recipientIds.map((recipientId) => this.createFor(recipientId, input, tenantId)));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Mark rows read — recipient-scoped. `id` omitted → all unread rows for the
|
|
129
|
+
* recipient; `id` given → that one row, still scoped to the recipient. Scoped
|
|
130
|
+
* to unread so a re-mark never overwrites an earlier `read_at` timestamp.
|
|
131
|
+
* Sets whichever read-state column(s) the model declares.
|
|
132
|
+
*/
|
|
133
|
+
markRead(recipientId, id) {
|
|
134
|
+
const { isRead, readAt } = this.columns;
|
|
135
|
+
const unreadScope = isRead ? { isRead: false } : { readAt: null };
|
|
136
|
+
const data = {};
|
|
137
|
+
if (isRead) data.isRead = true;
|
|
138
|
+
if (readAt) data.readAt = /* @__PURE__ */ new Date();
|
|
139
|
+
return this.updateMany(this.toRow({
|
|
140
|
+
recipientId,
|
|
141
|
+
...unreadScope,
|
|
142
|
+
...id !== void 0 ? { id } : {}
|
|
143
|
+
}), this.toRow(data));
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Inverse of `markRead`, same recipient-scoping. No unread scope needed —
|
|
147
|
+
* clearing the read-state of an already-unread row is a no-op.
|
|
148
|
+
*/
|
|
149
|
+
markUnread(recipientId, id) {
|
|
150
|
+
const { isRead, readAt } = this.columns;
|
|
151
|
+
const data = {};
|
|
152
|
+
if (isRead) data.isRead = false;
|
|
153
|
+
if (readAt) data.readAt = null;
|
|
154
|
+
return this.updateMany(this.toRow({
|
|
155
|
+
recipientId,
|
|
156
|
+
...id !== void 0 ? { id } : {}
|
|
157
|
+
}), this.toRow(data));
|
|
158
|
+
}
|
|
159
|
+
/** Find one row for a recipient (read path — `filterBy` maps the keys). */
|
|
160
|
+
findFor(recipientId, id) {
|
|
161
|
+
return this.first({
|
|
162
|
+
recipientId,
|
|
163
|
+
id
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Delete rows — recipient-scoped. `id` omitted → clear all for the
|
|
168
|
+
* recipient; `id` given → that one row.
|
|
169
|
+
*/
|
|
170
|
+
deleteFor(recipientId, id) {
|
|
171
|
+
return this.deleteMany(this.toRow({
|
|
172
|
+
recipientId,
|
|
173
|
+
...id !== void 0 ? { id } : {}
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
/** Read path — `filterBy` maps `recipientId`/`idempotencyKey` to columns. */
|
|
177
|
+
firstByIdempotencyKey(recipientId, idempotencyKey) {
|
|
178
|
+
return this.first({
|
|
179
|
+
recipientId,
|
|
180
|
+
idempotencyKey
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
//#endregion
|
|
186
|
+
export { BaseNotificationsRepository };
|
|
187
|
+
//# sourceMappingURL=base-notifications-repository.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base-notifications-repository.mjs","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/in-app/base-notifications-repository.ts"],"sourcesContent":["/**\n * Shipped behavior repository for the in-app store.\n *\n * Concrete (NOT abstract) so `inApp.configure({ model })` can\n * `new BaseNotificationsRepository(model)` without subclassing. The dev only\n * extends this when they need extra query methods.\n *\n * ## The single source of truth: the model's `columnMap`\n *\n * The model declares its physical columns ONCE via `static columnMap`. The\n * constructor resolves that map and derives BOTH sides from it:\n *\n * - the READ path — `filterBy` maps logical keys (`recipientId`, `unread`,\n * `type`, …) onto the model's columns; `RepositoryManager` applies it.\n * - the WRITE / UPDATE / DELETE path — `createFor` / `markRead` / … translate\n * logical keys through the same `filterBy` via `toRow`.\n *\n * Because both come from one `columnMap`, reads and writes can never target\n * different columns. `unread` is mode-agnostic: it filters `is_read = false`\n * when the model has that flag, otherwise `read_at IS NULL`.\n *\n * @example default (SQL snake_case — no subclass needed)\n * inApp.configure({ model: Notification })\n *\n * @example custom columns — declare them on the model, not here\n * class Notification extends DatabaseNotification {\n * public static columnMap = { recipient: \"audience_id\", isRead: \"seen\" };\n * }\n */\nimport { type FilterRules, RepositoryManager } from \"@warlock.js/core\";\nimport type { NotificationContract } from \"../contracts\";\nimport type { DatabasePayload, Id } from \"../types\";\nimport {\n type NotificationColumnMapHost,\n type ResolvedNotificationColumnMap,\n resolveColumnMap,\n} from \"./column-map\";\nimport type { DatabaseNotification } from \"./database-notification\";\n\n/**\n * Filter shape — logical keys the repo understands. `filterBy` maps each to a\n * physical column (or a predicate) derived from the model's `columnMap`.\n */\nexport type NotificationsFilter = {\n id?: Id;\n type?: string;\n /** Mode-agnostic unread filter — resolves to the model's read-state column. */\n unread?: boolean;\n recipientId?: Id;\n idempotencyKey?: string;\n};\n\n/** Class form of a `DatabaseNotification` subclass — what `configure()` accepts. */\nexport type NotificationModelClass = (new (...args: any[]) => DatabaseNotification) &\n NotificationColumnMapHost;\n\nexport class BaseNotificationsRepository<\n TModel extends NotificationContract = NotificationContract,\n> extends RepositoryManager<TModel, NotificationsFilter> {\n /** Resolved physical columns for the bound model — the single source. */\n private readonly columns: ResolvedNotificationColumnMap;\n\n public filterBy: FilterRules;\n\n public constructor(model?: NotificationModelClass) {\n super();\n\n if (model) {\n this.source = model;\n }\n\n this.columns = resolveColumnMap(model?.columnMap);\n this.filterBy = this.buildFilterBy();\n }\n\n /** The tenant column for this model, or `undefined` for single-tenant. */\n public get tenantColumn(): string | undefined {\n return this.columns.tenant;\n }\n\n /**\n * Build `filterBy` from the resolved column map. `unread` is the\n * mode-agnostic read filter; `isRead` / `readAt` / `tenant` are exposed for\n * direct filtering only when the model declares those columns.\n */\n private buildFilterBy(): FilterRules {\n const { recipient, tenant, readAt, isRead } = this.columns;\n\n const rules: FilterRules = {\n id: \"=\",\n type: \"=\",\n recipientId: [\"=\", recipient],\n idempotencyKey: [\"=\", \"idempotency_key\"],\n unread: (value, query) => {\n if (value !== true && value !== \"true\") {\n return;\n }\n\n if (isRead) {\n query.where(isRead, false);\n } else if (readAt) {\n query.whereNull(readAt);\n }\n },\n };\n\n if (isRead) {\n rules.isRead = [\"=\", isRead];\n }\n\n if (readAt) {\n rules.readAt = [\"=\", readAt];\n }\n\n if (tenant) {\n rules.tenant = [\"=\", tenant];\n }\n\n return rules;\n }\n\n /**\n * Translate an object keyed by LOGICAL names into one keyed by PHYSICAL\n * columns, using `filterBy` as the lookup. Keys with no `[op, column]` tuple\n * (e.g. `title`, `body`, `payload`, `type`, `id`) pass through unchanged —\n * they're already physical. Used for every write / update / delete payload +\n * filter (the paths `RepositoryManager` does NOT map).\n */\n protected toRow(obj: Record<string, unknown>): Record<string, unknown> {\n const rules = this.filterBy as Record<string, unknown>;\n const row: Record<string, unknown> = {};\n\n for (const [logical, value] of Object.entries(obj)) {\n const rule = rules[logical];\n const column = Array.isArray(rule) ? (rule[1] as string) : logical;\n row[column] = value;\n }\n\n return row;\n }\n\n /**\n * Create one row for the recipient. New rows start unread; the tenant column\n * (when the model declares one) is written from `tenantId`, which the\n * database channel reads off the recipient.\n *\n * Idempotency: when `input.idempotencyKey` is set, find-or-create — return\n * the existing row instead of inserting a duplicate. A unique index on the\n * key is the backstop for the rare insert race (we re-fetch on conflict).\n */\n public async createFor(\n recipientId: Id,\n input: DatabasePayload,\n tenantId?: Id,\n ): Promise<TModel> {\n const { idempotencyKey } = input;\n\n if (idempotencyKey) {\n const existing = await this.firstByIdempotencyKey(recipientId, idempotencyKey);\n if (existing) {\n return existing;\n }\n }\n\n const row: Record<string, unknown> = { recipientId, ...input };\n\n if (this.columns.isRead) {\n row.isRead = false;\n }\n\n if (this.columns.tenant && tenantId !== undefined) {\n row.tenant = tenantId;\n }\n\n try {\n return await this.create(this.toRow(row));\n } catch (error) {\n // Lost the insert race to a concurrent send with the same key → re-fetch.\n if (idempotencyKey) {\n const existing = await this.firstByIdempotencyKey(recipientId, idempotencyKey);\n if (existing) {\n return existing;\n }\n }\n\n throw error;\n }\n }\n\n /**\n * Bulk-create one row per recipient sharing the same input + tenant. Reserved\n * for the Phase-2 `Channel.sendMany` fan-out hook; not on the per-recipient\n * send path yet.\n */\n public createManyFor(\n recipientIds: Id[],\n input: DatabasePayload,\n tenantId?: Id,\n ): Promise<TModel[]> {\n return Promise.all(\n recipientIds.map((recipientId) => this.createFor(recipientId, input, tenantId)),\n );\n }\n\n /**\n * Mark rows read — recipient-scoped. `id` omitted → all unread rows for the\n * recipient; `id` given → that one row, still scoped to the recipient. Scoped\n * to unread so a re-mark never overwrites an earlier `read_at` timestamp.\n * Sets whichever read-state column(s) the model declares.\n */\n public markRead(recipientId: Id, id?: Id): Promise<number> {\n const { isRead, readAt } = this.columns;\n const unreadScope = isRead ? { isRead: false } : { readAt: null };\n\n const data: Record<string, unknown> = {};\n\n if (isRead) {\n data.isRead = true;\n }\n\n if (readAt) {\n data.readAt = new Date();\n }\n\n return this.updateMany(\n this.toRow({ recipientId, ...unreadScope, ...(id !== undefined ? { id } : {}) }),\n this.toRow(data),\n );\n }\n\n /**\n * Inverse of `markRead`, same recipient-scoping. No unread scope needed —\n * clearing the read-state of an already-unread row is a no-op.\n */\n public markUnread(recipientId: Id, id?: Id): Promise<number> {\n const { isRead, readAt } = this.columns;\n const data: Record<string, unknown> = {};\n\n if (isRead) {\n data.isRead = false;\n }\n\n if (readAt) {\n data.readAt = null;\n }\n\n return this.updateMany(\n this.toRow({ recipientId, ...(id !== undefined ? { id } : {}) }),\n this.toRow(data),\n );\n }\n\n /** Find one row for a recipient (read path — `filterBy` maps the keys). */\n public findFor(recipientId: Id, id: Id): Promise<TModel | null> {\n return this.first({ recipientId, id } as NotificationsFilter);\n }\n\n /**\n * Delete rows — recipient-scoped. `id` omitted → clear all for the\n * recipient; `id` given → that one row.\n */\n public deleteFor(recipientId: Id, id?: Id): Promise<number> {\n return this.deleteMany(this.toRow({ recipientId, ...(id !== undefined ? { id } : {}) }));\n }\n\n /** Read path — `filterBy` maps `recipientId`/`idempotencyKey` to columns. */\n private firstByIdempotencyKey(recipientId: Id, idempotencyKey: string): Promise<TModel | null> {\n return this.first({ recipientId, idempotencyKey } as NotificationsFilter);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,IAAa,8BAAb,cAEU,kBAA+C;;CAEvD,AAAiB;CAEjB,AAAO;CAEP,AAAO,YAAY,OAAgC;EACjD,MAAM;EAEN,IAAI,OACF,KAAK,SAAS;EAGhB,KAAK,UAAU,iBAAiB,OAAO,SAAS;EAChD,KAAK,WAAW,KAAK,cAAc;CACrC;;CAGA,IAAW,eAAmC;EAC5C,OAAO,KAAK,QAAQ;CACtB;;;;;;CAOA,AAAQ,gBAA6B;EACnC,MAAM,EAAE,WAAW,QAAQ,QAAQ,WAAW,KAAK;EAEnD,MAAM,QAAqB;GACzB,IAAI;GACJ,MAAM;GACN,aAAa,CAAC,KAAK,SAAS;GAC5B,gBAAgB,CAAC,KAAK,iBAAiB;GACvC,SAAS,OAAO,UAAU;IACxB,IAAI,UAAU,QAAQ,UAAU,QAC9B;IAGF,IAAI,QACF,MAAM,MAAM,QAAQ,KAAK;SACpB,IAAI,QACT,MAAM,UAAU,MAAM;GAE1B;EACF;EAEA,IAAI,QACF,MAAM,SAAS,CAAC,KAAK,MAAM;EAG7B,IAAI,QACF,MAAM,SAAS,CAAC,KAAK,MAAM;EAG7B,IAAI,QACF,MAAM,SAAS,CAAC,KAAK,MAAM;EAG7B,OAAO;CACT;;;;;;;;CASA,AAAU,MAAM,KAAuD;EACrE,MAAM,QAAQ,KAAK;EACnB,MAAM,MAA+B,CAAC;EAEtC,KAAK,MAAM,CAAC,SAAS,UAAU,OAAO,QAAQ,GAAG,GAAG;GAClD,MAAM,OAAO,MAAM;GACnB,MAAM,SAAS,MAAM,QAAQ,IAAI,IAAK,KAAK,KAAgB;GAC3D,IAAI,UAAU;EAChB;EAEA,OAAO;CACT;;;;;;;;;;CAWA,MAAa,UACX,aACA,OACA,UACiB;EACjB,MAAM,EAAE,mBAAmB;EAE3B,IAAI,gBAAgB;GAClB,MAAM,WAAW,MAAM,KAAK,sBAAsB,aAAa,cAAc;GAC7E,IAAI,UACF,OAAO;EAEX;EAEA,MAAM,MAA+B;GAAE;GAAa,GAAG;EAAM;EAE7D,IAAI,KAAK,QAAQ,QACf,IAAI,SAAS;EAGf,IAAI,KAAK,QAAQ,UAAU,aAAa,QACtC,IAAI,SAAS;EAGf,IAAI;GACF,OAAO,MAAM,KAAK,OAAO,KAAK,MAAM,GAAG,CAAC;EAC1C,SAAS,OAAO;GAEd,IAAI,gBAAgB;IAClB,MAAM,WAAW,MAAM,KAAK,sBAAsB,aAAa,cAAc;IAC7E,IAAI,UACF,OAAO;GAEX;GAEA,MAAM;EACR;CACF;;;;;;CAOA,AAAO,cACL,cACA,OACA,UACmB;EACnB,OAAO,QAAQ,IACb,aAAa,KAAK,gBAAgB,KAAK,UAAU,aAAa,OAAO,QAAQ,CAAC,CAChF;CACF;;;;;;;CAQA,AAAO,SAAS,aAAiB,IAA0B;EACzD,MAAM,EAAE,QAAQ,WAAW,KAAK;EAChC,MAAM,cAAc,SAAS,EAAE,QAAQ,MAAM,IAAI,EAAE,QAAQ,KAAK;EAEhE,MAAM,OAAgC,CAAC;EAEvC,IAAI,QACF,KAAK,SAAS;EAGhB,IAAI,QACF,KAAK,yBAAS,IAAI,KAAK;EAGzB,OAAO,KAAK,WACV,KAAK,MAAM;GAAE;GAAa,GAAG;GAAa,GAAI,OAAO,SAAY,EAAE,GAAG,IAAI,CAAC;EAAG,CAAC,GAC/E,KAAK,MAAM,IAAI,CACjB;CACF;;;;;CAMA,AAAO,WAAW,aAAiB,IAA0B;EAC3D,MAAM,EAAE,QAAQ,WAAW,KAAK;EAChC,MAAM,OAAgC,CAAC;EAEvC,IAAI,QACF,KAAK,SAAS;EAGhB,IAAI,QACF,KAAK,SAAS;EAGhB,OAAO,KAAK,WACV,KAAK,MAAM;GAAE;GAAa,GAAI,OAAO,SAAY,EAAE,GAAG,IAAI,CAAC;EAAG,CAAC,GAC/D,KAAK,MAAM,IAAI,CACjB;CACF;;CAGA,AAAO,QAAQ,aAAiB,IAAgC;EAC9D,OAAO,KAAK,MAAM;GAAE;GAAa;EAAG,CAAwB;CAC9D;;;;;CAMA,AAAO,UAAU,aAAiB,IAA0B;EAC1D,OAAO,KAAK,WAAW,KAAK,MAAM;GAAE;GAAa,GAAI,OAAO,SAAY,EAAE,GAAG,IAAI,CAAC;EAAG,CAAC,CAAC;CACzF;;CAGA,AAAQ,sBAAsB,aAAiB,gBAAgD;EAC7F,OAAO,KAAK,MAAM;GAAE;GAAa;EAAe,CAAwB;CAC1E;AACF"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//#region ../@warlock.js/notifications/src/in-app/column-map.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `columnMap` — how a notification model's logical roles map to the physical
|
|
4
|
+
* columns of ITS table. The ONE source the model getters, the repository
|
|
5
|
+
* (read filter + write mapping), the database channel, and the migration
|
|
6
|
+
* factory all read, so they can never drift apart.
|
|
7
|
+
*
|
|
8
|
+
* Declared as a `static columnMap` on the model and ejected to userland, so
|
|
9
|
+
* the dev owns it:
|
|
10
|
+
*
|
|
11
|
+
* @RegisterModel()
|
|
12
|
+
* export class Notification extends DatabaseNotification {
|
|
13
|
+
* public static table = "notifications";
|
|
14
|
+
* public static columnMap: NotificationColumnMap = {
|
|
15
|
+
* tenant: "organization_id", // omit → single-tenant
|
|
16
|
+
* readAt: "read_at", // read-state (see below)
|
|
17
|
+
* };
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* ## Read-state is chosen by which keys are PRESENT
|
|
21
|
+
*
|
|
22
|
+
* - `readAt` only → unread = `read_at IS NULL`; marking read stamps it.
|
|
23
|
+
* - `isRead` only → unread = `is_read = false`; no timestamp.
|
|
24
|
+
* - both → `is_read` is the indexed flag used for filtering, `read_at`
|
|
25
|
+
* records WHEN; marking read sets both.
|
|
26
|
+
*
|
|
27
|
+
* Declare neither and you get the `readAt`-only default — so a model can omit
|
|
28
|
+
* `columnMap` entirely and still work.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The logical → physical column bindings a notification model may declare.
|
|
32
|
+
* Every key is optional; `resolveColumnMap` fills the defaults. Values are the
|
|
33
|
+
* app's real column names (rename-friendly: `readAt: "seen_at"`).
|
|
34
|
+
*/
|
|
35
|
+
type NotificationColumnMap = {
|
|
36
|
+
/** Recipient FK column. Default: `"user_id"`. */recipient?: string; /** Multi-tenant scope column, written from the recipient. Omit → single-tenant. */
|
|
37
|
+
tenant?: string; /** Read-timestamp column. Present → read-state records "when". */
|
|
38
|
+
readAt?: string; /** Read-flag column (indexed → fast unread filter). Present → read-state is a boolean. */
|
|
39
|
+
isRead?: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A `NotificationColumnMap` with the defaults applied. `recipient` is always
|
|
43
|
+
* set, and at least one of `readAt` / `isRead` is always present (the resolver
|
|
44
|
+
* falls back to `readAt`), so consumers never face a "no read-state" map.
|
|
45
|
+
*/
|
|
46
|
+
type ResolvedNotificationColumnMap = {
|
|
47
|
+
recipient: string;
|
|
48
|
+
tenant?: string;
|
|
49
|
+
readAt?: string;
|
|
50
|
+
isRead?: string;
|
|
51
|
+
};
|
|
52
|
+
/** A model class that may carry the `columnMap` static. */
|
|
53
|
+
type NotificationColumnMapHost = {
|
|
54
|
+
columnMap?: NotificationColumnMap;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a model's declared `columnMap` into a complete map: default the
|
|
58
|
+
* recipient column, and fall back to a `read_at` timestamp when the model
|
|
59
|
+
* declares no read-state column. The fallback guarantees the result always
|
|
60
|
+
* has a usable read-state representation, so no boot-time validation is needed.
|
|
61
|
+
*/
|
|
62
|
+
declare function resolveColumnMap(map: NotificationColumnMap | undefined): ResolvedNotificationColumnMap;
|
|
63
|
+
//#endregion
|
|
64
|
+
export { NotificationColumnMap, NotificationColumnMapHost, ResolvedNotificationColumnMap, resolveColumnMap };
|
|
65
|
+
//# sourceMappingURL=column-map.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"column-map.d.mts","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/in-app/column-map.ts"],"mappings":";;AAkCA;;;;;;;;;AAQQ;AAQR;;;;;;;;;AAIQ;AAIR;;;;AACmC;AAYnC;;;;;;;KArCY,qBAAA;EAuCoB,iDArC9B,SAAA;EAEA,MAAA;EAEA,MAAA;EAEA,MAAA;AAAA;;;;;;KAQU,6BAAA;EACV,SAAA;EACA,MAAA;EACA,MAAA;EACA,MAAA;AAAA;;KAIU,yBAAA;EACV,SAAA,GAAY,qBAAqB;AAAA;;;;;;;iBAYnB,gBAAA,CACd,GAAA,EAAK,qBAAA,eACJ,6BAA6B"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region ../@warlock.js/notifications/src/in-app/column-map.ts
|
|
2
|
+
const DEFAULT_RECIPIENT_COLUMN = "user_id";
|
|
3
|
+
const DEFAULT_READ_AT_COLUMN = "read_at";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a model's declared `columnMap` into a complete map: default the
|
|
6
|
+
* recipient column, and fall back to a `read_at` timestamp when the model
|
|
7
|
+
* declares no read-state column. The fallback guarantees the result always
|
|
8
|
+
* has a usable read-state representation, so no boot-time validation is needed.
|
|
9
|
+
*/
|
|
10
|
+
function resolveColumnMap(map) {
|
|
11
|
+
const recipient = map?.recipient ?? DEFAULT_RECIPIENT_COLUMN;
|
|
12
|
+
const tenant = map?.tenant;
|
|
13
|
+
if (!map?.readAt && !map?.isRead) return {
|
|
14
|
+
recipient,
|
|
15
|
+
tenant,
|
|
16
|
+
readAt: DEFAULT_READ_AT_COLUMN
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
recipient,
|
|
20
|
+
tenant,
|
|
21
|
+
readAt: map.readAt,
|
|
22
|
+
isRead: map.isRead
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { resolveColumnMap };
|
|
28
|
+
//# sourceMappingURL=column-map.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"column-map.mjs","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/in-app/column-map.ts"],"sourcesContent":["/**\n * `columnMap` — how a notification model's logical roles map to the physical\n * columns of ITS table. The ONE source the model getters, the repository\n * (read filter + write mapping), the database channel, and the migration\n * factory all read, so they can never drift apart.\n *\n * Declared as a `static columnMap` on the model and ejected to userland, so\n * the dev owns it:\n *\n * @RegisterModel()\n * export class Notification extends DatabaseNotification {\n * public static table = \"notifications\";\n * public static columnMap: NotificationColumnMap = {\n * tenant: \"organization_id\", // omit → single-tenant\n * readAt: \"read_at\", // read-state (see below)\n * };\n * }\n *\n * ## Read-state is chosen by which keys are PRESENT\n *\n * - `readAt` only → unread = `read_at IS NULL`; marking read stamps it.\n * - `isRead` only → unread = `is_read = false`; no timestamp.\n * - both → `is_read` is the indexed flag used for filtering, `read_at`\n * records WHEN; marking read sets both.\n *\n * Declare neither and you get the `readAt`-only default — so a model can omit\n * `columnMap` entirely and still work.\n */\n\n/**\n * The logical → physical column bindings a notification model may declare.\n * Every key is optional; `resolveColumnMap` fills the defaults. Values are the\n * app's real column names (rename-friendly: `readAt: \"seen_at\"`).\n */\nexport type NotificationColumnMap = {\n /** Recipient FK column. Default: `\"user_id\"`. */\n recipient?: string;\n /** Multi-tenant scope column, written from the recipient. Omit → single-tenant. */\n tenant?: string;\n /** Read-timestamp column. Present → read-state records \"when\". */\n readAt?: string;\n /** Read-flag column (indexed → fast unread filter). Present → read-state is a boolean. */\n isRead?: string;\n};\n\n/**\n * A `NotificationColumnMap` with the defaults applied. `recipient` is always\n * set, and at least one of `readAt` / `isRead` is always present (the resolver\n * falls back to `readAt`), so consumers never face a \"no read-state\" map.\n */\nexport type ResolvedNotificationColumnMap = {\n recipient: string;\n tenant?: string;\n readAt?: string;\n isRead?: string;\n};\n\n/** A model class that may carry the `columnMap` static. */\nexport type NotificationColumnMapHost = {\n columnMap?: NotificationColumnMap;\n};\n\nconst DEFAULT_RECIPIENT_COLUMN = \"user_id\";\nconst DEFAULT_READ_AT_COLUMN = \"read_at\";\n\n/**\n * Resolve a model's declared `columnMap` into a complete map: default the\n * recipient column, and fall back to a `read_at` timestamp when the model\n * declares no read-state column. The fallback guarantees the result always\n * has a usable read-state representation, so no boot-time validation is needed.\n */\nexport function resolveColumnMap(\n map: NotificationColumnMap | undefined,\n): ResolvedNotificationColumnMap {\n const recipient = map?.recipient ?? DEFAULT_RECIPIENT_COLUMN;\n const tenant = map?.tenant;\n\n if (!map?.readAt && !map?.isRead) {\n return { recipient, tenant, readAt: DEFAULT_READ_AT_COLUMN };\n }\n\n return {\n recipient,\n tenant,\n readAt: map.readAt,\n isRead: map.isRead,\n };\n}\n"],"mappings":";AA8DA,MAAM,2BAA2B;AACjC,MAAM,yBAAyB;;;;;;;AAQ/B,SAAgB,iBACd,KAC+B;CAC/B,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,SAAS,KAAK;CAEpB,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,QACxB,OAAO;EAAE;EAAW;EAAQ,QAAQ;CAAuB;CAG7D,OAAO;EACL;EACA;EACA,QAAQ,IAAI;EACZ,QAAQ,IAAI;CACd;AACF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Id } from "../types.mjs";
|
|
2
|
+
import { NotificationContract } from "../contracts/notification.contract.mjs";
|
|
3
|
+
import { NotificationColumnMap, ResolvedNotificationColumnMap } from "./column-map.mjs";
|
|
4
|
+
import { Model } from "@warlock.js/cascade";
|
|
5
|
+
|
|
6
|
+
//#region ../@warlock.js/notifications/src/in-app/database-notification.d.ts
|
|
7
|
+
declare abstract class DatabaseNotification extends Model implements NotificationContract {
|
|
8
|
+
/**
|
|
9
|
+
* Physical column bindings for THIS model's table. Override in the subclass;
|
|
10
|
+
* the empty default resolves to `{ recipient: "user_id", readAt: "read_at" }`.
|
|
11
|
+
*/
|
|
12
|
+
static columnMap: NotificationColumnMap;
|
|
13
|
+
/** The resolved column map for this row's model — defaults applied. */
|
|
14
|
+
protected get columns(): ResolvedNotificationColumnMap;
|
|
15
|
+
get recipientId(): Id;
|
|
16
|
+
/** Tenant scope value, or `undefined` for single-tenant models. */
|
|
17
|
+
get tenantId(): Id | undefined;
|
|
18
|
+
get type(): string;
|
|
19
|
+
get isRead(): boolean;
|
|
20
|
+
get readAt(): Date | null;
|
|
21
|
+
/**
|
|
22
|
+
* Mark this row read. Sets whichever read-state column(s) the model
|
|
23
|
+
* declares — the boolean (fast unread filtering) and/or the timestamp
|
|
24
|
+
* (when it was read). Persists via `save()`.
|
|
25
|
+
*/
|
|
26
|
+
markRead(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { DatabaseNotification };
|
|
30
|
+
//# sourceMappingURL=database-notification.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database-notification.d.mts","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/in-app/database-notification.ts"],"mappings":";;;;;;uBA6BsB,oBAAA,SAA6B,KAAA,YAAiB,oBAAA;EAQzC;;;;EAAA,OAHX,SAAA,EAAW,qBAAA;EAiBd;EAAA,cAdG,OAAA,IAAW,6BAAA;EAAA,IAId,WAAA,IAAe,EAAA;EAwBL;EAAA,IAnBV,QAAA,IAAY,EAAA;EAAA,IAKZ,IAAA;EAAA,IAIA,MAAA;EAAA,IAUA,MAAA,IAAU,IAAA;;;;;;EAUR,QAAA,IAAY,OAAA;AAAA"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { resolveColumnMap } from "./column-map.mjs";
|
|
2
|
+
import { Model } from "@warlock.js/cascade";
|
|
3
|
+
|
|
4
|
+
//#region ../@warlock.js/notifications/src/in-app/database-notification.ts
|
|
5
|
+
/**
|
|
6
|
+
* Shipped BASE in-app notification model. The scaffolded user model
|
|
7
|
+
* `extends DatabaseNotification`, sets `static table`, and declares its
|
|
8
|
+
* physical columns ONCE via `static columnMap`. Every accessor below derives
|
|
9
|
+
* from that map, so the model, the repository, and the migration agree by
|
|
10
|
+
* construction.
|
|
11
|
+
*
|
|
12
|
+
* The package ships NO concrete table or migration — those eject to userland
|
|
13
|
+
* (thin eject). See `notificationColumns()` for the matching migration factory.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* @RegisterModel()
|
|
17
|
+
* export class Notification extends DatabaseNotification {
|
|
18
|
+
* public static table = "notifications";
|
|
19
|
+
* public static columnMap: NotificationColumnMap = {
|
|
20
|
+
* tenant: "organization_id",
|
|
21
|
+
* readAt: "read_at",
|
|
22
|
+
* };
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
var DatabaseNotification = class extends Model {
|
|
26
|
+
/**
|
|
27
|
+
* Physical column bindings for THIS model's table. Override in the subclass;
|
|
28
|
+
* the empty default resolves to `{ recipient: "user_id", readAt: "read_at" }`.
|
|
29
|
+
*/
|
|
30
|
+
static columnMap = {};
|
|
31
|
+
/** The resolved column map for this row's model — defaults applied. */
|
|
32
|
+
get columns() {
|
|
33
|
+
return resolveColumnMap(this.constructor.columnMap);
|
|
34
|
+
}
|
|
35
|
+
get recipientId() {
|
|
36
|
+
return this.get(this.columns.recipient);
|
|
37
|
+
}
|
|
38
|
+
/** Tenant scope value, or `undefined` for single-tenant models. */
|
|
39
|
+
get tenantId() {
|
|
40
|
+
const { tenant } = this.columns;
|
|
41
|
+
return tenant ? this.get(tenant) : void 0;
|
|
42
|
+
}
|
|
43
|
+
get type() {
|
|
44
|
+
return this.get("type");
|
|
45
|
+
}
|
|
46
|
+
get isRead() {
|
|
47
|
+
const { isRead, readAt } = this.columns;
|
|
48
|
+
if (isRead) return Boolean(this.get(isRead));
|
|
49
|
+
return readAt ? this.get(readAt) != null : false;
|
|
50
|
+
}
|
|
51
|
+
get readAt() {
|
|
52
|
+
const { readAt } = this.columns;
|
|
53
|
+
return readAt ? this.get(readAt) ?? null : null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Mark this row read. Sets whichever read-state column(s) the model
|
|
57
|
+
* declares — the boolean (fast unread filtering) and/or the timestamp
|
|
58
|
+
* (when it was read). Persists via `save()`.
|
|
59
|
+
*/
|
|
60
|
+
async markRead() {
|
|
61
|
+
const { isRead, readAt } = this.columns;
|
|
62
|
+
if (isRead) this.set(isRead, true);
|
|
63
|
+
if (readAt) this.set(readAt, /* @__PURE__ */ new Date());
|
|
64
|
+
await this.save();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
//#endregion
|
|
69
|
+
export { DatabaseNotification };
|
|
70
|
+
//# sourceMappingURL=database-notification.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database-notification.mjs","names":[],"sources":["../../../../../../../@warlock.js/notifications/src/in-app/database-notification.ts"],"sourcesContent":["/**\n * Shipped BASE in-app notification model. The scaffolded user model\n * `extends DatabaseNotification`, sets `static table`, and declares its\n * physical columns ONCE via `static columnMap`. Every accessor below derives\n * from that map, so the model, the repository, and the migration agree by\n * construction.\n *\n * The package ships NO concrete table or migration — those eject to userland\n * (thin eject). See `notificationColumns()` for the matching migration factory.\n *\n * @example\n * @RegisterModel()\n * export class Notification extends DatabaseNotification {\n * public static table = \"notifications\";\n * public static columnMap: NotificationColumnMap = {\n * tenant: \"organization_id\",\n * readAt: \"read_at\",\n * };\n * }\n */\nimport { Model } from \"@warlock.js/cascade\";\nimport type { NotificationContract } from \"../contracts\";\nimport type { Id } from \"../types\";\nimport {\n type NotificationColumnMap,\n type ResolvedNotificationColumnMap,\n resolveColumnMap,\n} from \"./column-map\";\n\nexport abstract class DatabaseNotification extends Model implements NotificationContract {\n /**\n * Physical column bindings for THIS model's table. Override in the subclass;\n * the empty default resolves to `{ recipient: \"user_id\", readAt: \"read_at\" }`.\n */\n public static columnMap: NotificationColumnMap = {};\n\n /** The resolved column map for this row's model — defaults applied. */\n protected get columns(): ResolvedNotificationColumnMap {\n return resolveColumnMap((this.constructor as typeof DatabaseNotification).columnMap);\n }\n\n public get recipientId(): Id {\n return this.get(this.columns.recipient);\n }\n\n /** Tenant scope value, or `undefined` for single-tenant models. */\n public get tenantId(): Id | undefined {\n const { tenant } = this.columns;\n return tenant ? this.get(tenant) : undefined;\n }\n\n public get type(): string {\n return this.get(\"type\");\n }\n\n public get isRead(): boolean {\n const { isRead, readAt } = this.columns;\n\n if (isRead) {\n return Boolean(this.get(isRead));\n }\n\n return readAt ? this.get(readAt) != null : false;\n }\n\n public get readAt(): Date | null {\n const { readAt } = this.columns;\n return readAt ? (this.get(readAt) ?? null) : null;\n }\n\n /**\n * Mark this row read. Sets whichever read-state column(s) the model\n * declares — the boolean (fast unread filtering) and/or the timestamp\n * (when it was read). Persists via `save()`.\n */\n public async markRead(): Promise<void> {\n const { isRead, readAt } = this.columns;\n\n if (isRead) {\n this.set(isRead, true);\n }\n\n if (readAt) {\n this.set(readAt, new Date());\n }\n\n await this.save();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA6BA,IAAsB,uBAAtB,cAAmD,MAAsC;;;;;CAKvF,OAAc,YAAmC,CAAC;;CAGlD,IAAc,UAAyC;EACrD,OAAO,iBAAkB,KAAK,YAA4C,SAAS;CACrF;CAEA,IAAW,cAAkB;EAC3B,OAAO,KAAK,IAAI,KAAK,QAAQ,SAAS;CACxC;;CAGA,IAAW,WAA2B;EACpC,MAAM,EAAE,WAAW,KAAK;EACxB,OAAO,SAAS,KAAK,IAAI,MAAM,IAAI;CACrC;CAEA,IAAW,OAAe;EACxB,OAAO,KAAK,IAAI,MAAM;CACxB;CAEA,IAAW,SAAkB;EAC3B,MAAM,EAAE,QAAQ,WAAW,KAAK;EAEhC,IAAI,QACF,OAAO,QAAQ,KAAK,IAAI,MAAM,CAAC;EAGjC,OAAO,SAAS,KAAK,IAAI,MAAM,KAAK,OAAO;CAC7C;CAEA,IAAW,SAAsB;EAC/B,MAAM,EAAE,WAAW,KAAK;EACxB,OAAO,SAAU,KAAK,IAAI,MAAM,KAAK,OAAQ;CAC/C;;;;;;CAOA,MAAa,WAA0B;EACrC,MAAM,EAAE,QAAQ,WAAW,KAAK;EAEhC,IAAI,QACF,KAAK,IAAI,QAAQ,IAAI;EAGvB,IAAI,QACF,KAAK,IAAI,wBAAQ,IAAI,KAAK,CAAC;EAG7B,MAAM,KAAK,KAAK;CAClB;AACF"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DatabasePayload, Id, Notifiable } from "../types.mjs";
|
|
2
|
+
import { Channel } from "../contracts/channel.contract.mjs";
|
|
3
|
+
import { NotificationContract } from "../contracts/notification.contract.mjs";
|
|
4
|
+
import { BaseNotificationsRepository, NotificationModelClass, NotificationsFilter } from "./base-notifications-repository.mjs";
|
|
5
|
+
import { TypedRepositoryOptionsWithPages } from "@warlock.js/core";
|
|
6
|
+
|
|
7
|
+
//#region ../@warlock.js/notifications/src/in-app/in-app.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* List options for the read methods — the repository's paginated options
|
|
10
|
+
* (`page` / `limit` / `orderBy` / filter keys). `recipientId` is always forced
|
|
11
|
+
* from the recipient argument, so passing it here has no effect.
|
|
12
|
+
*/
|
|
13
|
+
type NotificationsListOptions = TypedRepositoryOptionsWithPages<NotificationsFilter>;
|
|
14
|
+
/**
|
|
15
|
+
* Configure options — discriminated so `model` and `repository` are mutually
|
|
16
|
+
* exclusive at the type level. Future knobs (cache / realtime / naming) slot
|
|
17
|
+
* in here without breaking the signature.
|
|
18
|
+
*/
|
|
19
|
+
type ConfigureOptions = {
|
|
20
|
+
model: NotificationModelClass;
|
|
21
|
+
repository?: never;
|
|
22
|
+
} | {
|
|
23
|
+
repository: BaseNotificationsRepository;
|
|
24
|
+
model?: never;
|
|
25
|
+
};
|
|
26
|
+
declare class InApp {
|
|
27
|
+
private repo?;
|
|
28
|
+
/**
|
|
29
|
+
* Bind the in-app store AND return the `database` channel. Called once
|
|
30
|
+
* from `config/notifications.ts`. Subsequent calls REPLACE the binding.
|
|
31
|
+
*/
|
|
32
|
+
configure(options: ConfigureOptions): Channel<DatabasePayload>;
|
|
33
|
+
private get repository();
|
|
34
|
+
/** General list — scoped to recipient; pass `options` for paging + filters. */
|
|
35
|
+
list(recipient: Notifiable | Id, options?: NotificationsListOptions): any;
|
|
36
|
+
/** Unread-only list — common case for badges + dashboards. */
|
|
37
|
+
listUnread(recipient: Notifiable | Id, options?: NotificationsListOptions): any;
|
|
38
|
+
/**
|
|
39
|
+
* Cached unread count — backs the badge. Auto-invalidated by the repo's
|
|
40
|
+
* model create/update events (`RepositoryManager.registerEvents`).
|
|
41
|
+
*/
|
|
42
|
+
countUnread(recipient: Notifiable | Id): any;
|
|
43
|
+
/** Find one notification for a recipient — for a detail view. */
|
|
44
|
+
find(recipient: Notifiable | Id, id: Id): Promise<NotificationContract | null>;
|
|
45
|
+
/** Mark read — `id` omitted = all unread for this recipient. */
|
|
46
|
+
markAsRead(recipient: Notifiable | Id, id?: Id): Promise<number>;
|
|
47
|
+
/** Mark unread — same recipient-scoping. */
|
|
48
|
+
markAsUnread(recipient: Notifiable | Id, id?: Id): Promise<number>;
|
|
49
|
+
/** Delete/dismiss — `id` omitted = clear all for this recipient. */
|
|
50
|
+
dismiss(recipient: Notifiable | Id, id?: Id): Promise<number>;
|
|
51
|
+
}
|
|
52
|
+
/** Single in-app facade — bound at boot via `inApp.configure(...)`. */
|
|
53
|
+
declare const inApp: InApp;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { ConfigureOptions, NotificationsListOptions, inApp };
|
|
56
|
+
//# sourceMappingURL=in-app.d.mts.map
|