@warlock.js/notifications 4.8.2 → 4.9.1
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/cjs/index.cjs +18 -18
- package/cjs/index.cjs.map +1 -1
- package/esm/channels/database-channel.d.mts +1 -1
- package/esm/channels/database-channel.d.mts.map +1 -1
- package/esm/channels/database-channel.mjs +1 -1
- package/esm/channels/database-channel.mjs.map +1 -1
- package/esm/channels/mail-channel.d.mts +1 -1
- package/esm/channels/mail-channel.d.mts.map +1 -1
- package/esm/channels/mail-channel.mjs +1 -1
- package/esm/channels/mail-channel.mjs.map +1 -1
- package/esm/config.d.mts +1 -1
- package/esm/config.d.mts.map +1 -1
- package/esm/config.mjs +1 -1
- package/esm/config.mjs.map +1 -1
- package/esm/contracts/channel.contract.d.mts +1 -1
- package/esm/contracts/channel.contract.d.mts.map +1 -1
- package/esm/contracts/notification.contract.d.mts +1 -1
- package/esm/contracts/notification.contract.d.mts.map +1 -1
- package/esm/contracts/preference-provider.contract.d.mts +1 -1
- package/esm/contracts/preference-provider.contract.d.mts.map +1 -1
- package/esm/contracts/queue-dispatcher.contract.d.mts +1 -1
- package/esm/contracts/queue-dispatcher.contract.d.mts.map +1 -1
- package/esm/contracts/rate-limiter.contract.d.mts +1 -1
- package/esm/contracts/rate-limiter.contract.d.mts.map +1 -1
- package/esm/dispatch/define-channel.d.mts +1 -1
- package/esm/dispatch/define-channel.d.mts.map +1 -1
- package/esm/dispatch/define-channel.mjs +1 -1
- package/esm/dispatch/define-channel.mjs.map +1 -1
- package/esm/dispatch/define-notification.d.mts +1 -1
- package/esm/dispatch/define-notification.d.mts.map +1 -1
- package/esm/dispatch/define-notification.mjs +1 -1
- package/esm/dispatch/define-notification.mjs.map +1 -1
- package/esm/dispatch/dispatch-id.mjs +1 -1
- package/esm/dispatch/dispatch-id.mjs.map +1 -1
- package/esm/dispatch/dispatcher.mjs +1 -1
- package/esm/dispatch/dispatcher.mjs.map +1 -1
- package/esm/dispatch/notifications-event-bus.d.mts +1 -1
- package/esm/dispatch/notifications-event-bus.d.mts.map +1 -1
- package/esm/dispatch/notifications-event-bus.mjs +1 -1
- package/esm/dispatch/notifications-event-bus.mjs.map +1 -1
- package/esm/dispatch/notify.d.mts +1 -1
- package/esm/dispatch/notify.d.mts.map +1 -1
- package/esm/dispatch/notify.mjs +1 -1
- package/esm/dispatch/notify.mjs.map +1 -1
- package/esm/errors.d.mts +1 -1
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +1 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/in-app/base-notifications-repository.d.mts +1 -1
- package/esm/in-app/base-notifications-repository.d.mts.map +1 -1
- package/esm/in-app/base-notifications-repository.mjs +1 -1
- package/esm/in-app/base-notifications-repository.mjs.map +1 -1
- package/esm/in-app/column-map.d.mts +1 -1
- package/esm/in-app/column-map.d.mts.map +1 -1
- package/esm/in-app/column-map.mjs +1 -1
- package/esm/in-app/column-map.mjs.map +1 -1
- package/esm/in-app/database-notification.d.mts +1 -1
- package/esm/in-app/database-notification.d.mts.map +1 -1
- package/esm/in-app/database-notification.mjs +1 -1
- package/esm/in-app/database-notification.mjs.map +1 -1
- package/esm/in-app/in-app.d.mts +1 -1
- package/esm/in-app/in-app.d.mts.map +1 -1
- package/esm/in-app/in-app.mjs +1 -1
- package/esm/in-app/in-app.mjs.map +1 -1
- package/esm/migration/notification-columns.d.mts +1 -1
- package/esm/migration/notification-columns.d.mts.map +1 -1
- package/esm/migration/notification-columns.mjs +1 -1
- package/esm/migration/notification-columns.mjs.map +1 -1
- package/esm/queue/herald-queue.d.mts +1 -1
- package/esm/queue/herald-queue.d.mts.map +1 -1
- package/esm/queue/herald-queue.mjs +1 -1
- package/esm/queue/herald-queue.mjs.map +1 -1
- package/esm/queue/load-herald.d.mts +1 -1
- package/esm/queue/load-herald.d.mts.map +1 -1
- package/esm/queue/load-herald.mjs +1 -1
- package/esm/queue/load-herald.mjs.map +1 -1
- package/esm/queue/notifications-worker.d.mts +1 -1
- package/esm/queue/notifications-worker.d.mts.map +1 -1
- package/esm/queue/notifications-worker.mjs +1 -1
- package/esm/queue/notifications-worker.mjs.map +1 -1
- package/esm/types.d.mts +1 -1
- package/esm/types.d.mts.map +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"column-map.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"column-map.mjs","names":[],"sources":["../../../../../../../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"}
|
|
@@ -3,7 +3,7 @@ import { NotificationContract } from "../contracts/notification.contract.mjs";
|
|
|
3
3
|
import { NotificationColumnMap, ResolvedNotificationColumnMap } from "./column-map.mjs";
|
|
4
4
|
import { Model } from "@warlock.js/cascade";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../notifications/src/in-app/database-notification.d.ts
|
|
7
7
|
declare abstract class DatabaseNotification extends Model implements NotificationContract {
|
|
8
8
|
/**
|
|
9
9
|
* Physical column bindings for THIS model's table. Override in the subclass;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-notification.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"database-notification.d.mts","names":[],"sources":["../../../../../../../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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveColumnMap } from "./column-map.mjs";
|
|
2
2
|
import { Model } from "@warlock.js/cascade";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../notifications/src/in-app/database-notification.ts
|
|
5
5
|
/**
|
|
6
6
|
* Shipped BASE in-app notification model. The scaffolded user model
|
|
7
7
|
* `extends DatabaseNotification`, sets `static table`, and declares its
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-notification.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"database-notification.mjs","names":[],"sources":["../../../../../../../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"}
|
package/esm/in-app/in-app.d.mts
CHANGED
|
@@ -4,7 +4,7 @@ import { NotificationContract } from "../contracts/notification.contract.mjs";
|
|
|
4
4
|
import { BaseNotificationsRepository, NotificationModelClass, NotificationsFilter } from "./base-notifications-repository.mjs";
|
|
5
5
|
import { TypedRepositoryOptionsWithPages } from "@warlock.js/core";
|
|
6
6
|
|
|
7
|
-
//#region
|
|
7
|
+
//#region ../notifications/src/in-app/in-app.d.ts
|
|
8
8
|
/**
|
|
9
9
|
* List options for the read methods — the repository's paginated options
|
|
10
10
|
* (`page` / `limit` / `orderBy` / filter keys). `recipientId` is always forced
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-app.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"in-app.d.mts","names":[],"sources":["../../../../../../../notifications/src/in-app/in-app.ts"],"mappings":";;;;;;;;;;;;KA4CY,wBAAA,GAA2B,+BAA+B,CAAC,mBAAA;;;;;;KAO3D,gBAAA;EACN,KAAA,EAAO,sBAAA;EAAwB,UAAA;AAAA;EAC/B,UAAA,EAAY,2BAA2B;EAAE,KAAA;AAAA;AAAA,cAKzC,KAAA;EAAA,QACI,IAAA;EA0D+B;;;;EApDhC,SAAA,CAAU,OAAA,EAAS,gBAAA,GAAmB,OAAA,CAAQ,eAAA;EAAA,YAQzC,UAAA;EARL;EAmBA,IAAA,CAAK,SAAA,EAAW,UAAA,GAAa,EAAA,EAAI,OAAA,GAAU,wBAAA;EAnBjC;EAwBV,UAAA,CAAW,SAAA,EAAW,UAAA,GAAa,EAAA,EAAI,OAAA,GAAU,wBAAA;EAxBH;;;;EAgC9C,WAAA,CAAY,SAAA,EAAW,UAAA,GAAa,EAAA;EAb/B;EAkBL,IAAA,CAAK,SAAA,EAAW,UAAA,GAAa,EAAA,EAAI,EAAA,EAAI,EAAA,GAAE,OAAA,CAAA,oBAAA;EAlBN;EAuBjC,UAAA,CAAW,SAAA,EAAW,UAAA,GAAa,EAAA,EAAI,EAAA,GAAK,EAAA,GAAE,OAAA;EAlBxB;EAuBtB,YAAA,CAAa,SAAA,EAAW,UAAA,GAAa,EAAA,EAAI,EAAA,GAAK,EAAA,GAAE,OAAA;EAvBrC;EA4BX,OAAA,CAAQ,SAAA,EAAW,UAAA,GAAa,EAAA,EAAI,EAAA,GAAK,EAAA,GAAE,OAAA;AAAA;;cAMvC,KAAA,EAAK,KAAc"}
|
package/esm/in-app/in-app.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { databaseChannel } from "../channels/database-channel.mjs";
|
|
2
2
|
import { BaseNotificationsRepository } from "./base-notifications-repository.mjs";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../notifications/src/in-app/in-app.ts
|
|
5
5
|
const idOf = (recipient) => typeof recipient === "object" ? recipient.id : recipient;
|
|
6
6
|
var InApp = class {
|
|
7
7
|
repo;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-app.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"in-app.mjs","names":[],"sources":["../../../../../../../notifications/src/in-app/in-app.ts"],"sourcesContent":["/**\n * `inApp` — app-facing facade for the in-app/database channel.\n *\n * `inApp.configure({ model | repository })` binds the in-app store AND\n * returns the `database` channel — so the read side and the dispatched\n * write side share ONE repository instance.\n * • `{ model: Notification }` — 90% case; default repo built internally.\n * • `{ repository: myRepo }` — 10% case; custom column mapping / extras.\n *\n * No zero-arg form: the package ships no concrete model/table (thin eject),\n * so the dev MUST tell us which model to use.\n *\n * Every read + mutation is RECIPIENT-SCOPED by construction — `markAsRead(user,\n * id)` cannot flip a row belonging to a different recipient even if handed a\n * foreign id (the recipient id is forced into the filter → 0 rows match).\n *\n * @example\n * // src/config/notifications.ts:\n * inApp.configure({ model: Notification }),\n *\n * // anywhere:\n * const unread = await inApp.listUnread(user);\n * const badge = await inApp.countUnread(user);\n * const one = await inApp.find(user, \"ntf_123\");\n * await inApp.markAsRead(user, \"ntf_123\");\n * await inApp.markAsRead(user); // mark ALL unread\n * await inApp.dismiss(user, \"ntf_123\"); // delete one\n * await inApp.dismiss(user); // clear all for this recipient\n */\nimport type { TypedRepositoryOptionsWithPages } from \"@warlock.js/core\";\nimport { databaseChannel } from \"../channels/database-channel\";\nimport type { Channel } from \"../contracts\";\nimport type { DatabasePayload, Id, Notifiable } from \"../types\";\nimport {\n BaseNotificationsRepository,\n type NotificationModelClass,\n type NotificationsFilter,\n} from \"./base-notifications-repository\";\n\n/**\n * List options for the read methods — the repository's paginated options\n * (`page` / `limit` / `orderBy` / filter keys). `recipientId` is always forced\n * from the recipient argument, so passing it here has no effect.\n */\nexport type NotificationsListOptions = TypedRepositoryOptionsWithPages<NotificationsFilter>;\n\n/**\n * Configure options — discriminated so `model` and `repository` are mutually\n * exclusive at the type level. Future knobs (cache / realtime / naming) slot\n * in here without breaking the signature.\n */\nexport type ConfigureOptions =\n | { model: NotificationModelClass; repository?: never }\n | { repository: BaseNotificationsRepository; model?: never };\n\nconst idOf = (recipient: Notifiable | Id): Id =>\n typeof recipient === \"object\" ? recipient.id : recipient;\n\nclass InApp {\n private repo?: BaseNotificationsRepository;\n\n /**\n * Bind the in-app store AND return the `database` channel. Called once\n * from `config/notifications.ts`. Subsequent calls REPLACE the binding.\n */\n public configure(options: ConfigureOptions): Channel<DatabasePayload> {\n this.repo =\n \"repository\" in options && options.repository\n ? options.repository\n : new BaseNotificationsRepository(options.model);\n return databaseChannel(this.repo);\n }\n\n private get repository(): BaseNotificationsRepository {\n if (!this.repo) {\n throw new Error(\n \"In-app notifications not configured — add \" +\n \"`database: inApp.configure({ model: Notification })` to config/notifications.ts\",\n );\n }\n return this.repo;\n }\n\n /** General list — scoped to recipient; pass `options` for paging + filters. */\n public list(recipient: Notifiable | Id, options?: NotificationsListOptions) {\n return this.repository.list({ ...options, recipientId: idOf(recipient) });\n }\n\n /** Unread-only list — common case for badges + dashboards. */\n public listUnread(recipient: Notifiable | Id, options?: NotificationsListOptions) {\n return this.repository.list({ ...options, recipientId: idOf(recipient), unread: true });\n }\n\n /**\n * Cached unread count — backs the badge. Auto-invalidated by the repo's\n * model create/update events (`RepositoryManager.registerEvents`).\n */\n public countUnread(recipient: Notifiable | Id) {\n return this.repository.countCached({ recipientId: idOf(recipient), unread: true });\n }\n\n /** Find one notification for a recipient — for a detail view. */\n public find(recipient: Notifiable | Id, id: Id) {\n return this.repository.findFor(idOf(recipient), id);\n }\n\n /** Mark read — `id` omitted = all unread for this recipient. */\n public markAsRead(recipient: Notifiable | Id, id?: Id) {\n return this.repository.markRead(idOf(recipient), id);\n }\n\n /** Mark unread — same recipient-scoping. */\n public markAsUnread(recipient: Notifiable | Id, id?: Id) {\n return this.repository.markUnread(idOf(recipient), id);\n }\n\n /** Delete/dismiss — `id` omitted = clear all for this recipient. */\n public dismiss(recipient: Notifiable | Id, id?: Id) {\n return this.repository.deleteFor(idOf(recipient), id);\n }\n}\n\n/** Single in-app facade — bound at boot via `inApp.configure(...)`. */\nexport const inApp = new InApp();\n"],"mappings":";;;;AAuDA,MAAM,QAAQ,cACZ,OAAO,cAAc,WAAW,UAAU,KAAK;AAEjD,IAAM,QAAN,MAAY;CACV,AAAQ;;;;;CAMR,AAAO,UAAU,SAAqD;EACpE,KAAK,OACH,gBAAgB,WAAW,QAAQ,aAC/B,QAAQ,aACR,IAAI,4BAA4B,QAAQ,KAAK;EACnD,OAAO,gBAAgB,KAAK,IAAI;CAClC;CAEA,IAAY,aAA0C;EACpD,IAAI,CAAC,KAAK,MACR,MAAM,IAAI,MACR,2HAEF;EAEF,OAAO,KAAK;CACd;;CAGA,AAAO,KAAK,WAA4B,SAAoC;EAC1E,OAAO,KAAK,WAAW,KAAK;GAAE,GAAG;GAAS,aAAa,KAAK,SAAS;EAAE,CAAC;CAC1E;;CAGA,AAAO,WAAW,WAA4B,SAAoC;EAChF,OAAO,KAAK,WAAW,KAAK;GAAE,GAAG;GAAS,aAAa,KAAK,SAAS;GAAG,QAAQ;EAAK,CAAC;CACxF;;;;;CAMA,AAAO,YAAY,WAA4B;EAC7C,OAAO,KAAK,WAAW,YAAY;GAAE,aAAa,KAAK,SAAS;GAAG,QAAQ;EAAK,CAAC;CACnF;;CAGA,AAAO,KAAK,WAA4B,IAAQ;EAC9C,OAAO,KAAK,WAAW,QAAQ,KAAK,SAAS,GAAG,EAAE;CACpD;;CAGA,AAAO,WAAW,WAA4B,IAAS;EACrD,OAAO,KAAK,WAAW,SAAS,KAAK,SAAS,GAAG,EAAE;CACrD;;CAGA,AAAO,aAAa,WAA4B,IAAS;EACvD,OAAO,KAAK,WAAW,WAAW,KAAK,SAAS,GAAG,EAAE;CACvD;;CAGA,AAAO,QAAQ,WAA4B,IAAS;EAClD,OAAO,KAAK,WAAW,UAAU,KAAK,SAAS,GAAG,EAAE;CACtD;AACF;;AAGA,MAAa,QAAQ,IAAI,MAAM"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NotificationColumnMapHost } from "../in-app/column-map.mjs";
|
|
2
2
|
import { ColumnMap } from "@warlock.js/cascade";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../notifications/src/migration/notification-columns.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Returns the column map for a notification table, named from the model's
|
|
7
7
|
* `columnMap`. Without a model, falls back to the resolved defaults
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-columns.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"notification-columns.d.mts","names":[],"sources":["../../../../../../../notifications/src/migration/notification-columns.ts"],"mappings":";;;;;;;;;iBAiCgB,mBAAA,CAAoB,KAAA,GAAQ,yBAAA,GAA4B,SAAS"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolveColumnMap } from "../in-app/column-map.mjs";
|
|
2
2
|
import { boolCol, json, string, text, timestamp, uuid } from "@warlock.js/cascade";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../notifications/src/migration/notification-columns.ts
|
|
5
5
|
/**
|
|
6
6
|
* Column factory for the notifications table, driven by the model's
|
|
7
7
|
* `columnMap`. The recipient / read-state / tenant columns take the NAMES the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-columns.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"notification-columns.mjs","names":[],"sources":["../../../../../../../notifications/src/migration/notification-columns.ts"],"sourcesContent":["/**\n * Column factory for the notifications table, driven by the model's\n * `columnMap`. The recipient / read-state / tenant columns take the NAMES the\n * model declares, so the table, the repository, and the model accessors all\n * agree by construction. The stable lexical columns (`type`, `title`, `body`,\n * `payload`, `idempotency_key`) keep fixed names.\n *\n * Read-state follows the map's presence rules (see `NotificationColumnMap`):\n * `readAt` → a nullable timestamp, `isRead` → an indexed boolean, both → both.\n *\n * Spread + extend for app-specific extras (FK references, composite indexes).\n *\n * @example\n * import { Migration } from \"@warlock.js/cascade\";\n * import { notificationColumns } from \"@warlock.js/notifications\";\n * import { Notification } from \"../notification.model\";\n *\n * export default Migration.create(Notification, notificationColumns(Notification));\n *\n * @example // with extras (multi-tenant FK)\n * export default Migration.create(Notification, {\n * ...notificationColumns(Notification),\n * organization_id: uuid().references(Organization.table).notNullable(),\n * });\n */\nimport { boolCol, type ColumnMap, json, string, text, timestamp, uuid } from \"@warlock.js/cascade\";\nimport { type NotificationColumnMapHost, resolveColumnMap } from \"../in-app/column-map\";\n\n/**\n * Returns the column map for a notification table, named from the model's\n * `columnMap`. Without a model, falls back to the resolved defaults\n * (`user_id` recipient + `read_at` read-state).\n */\nexport function notificationColumns(model?: NotificationColumnMapHost): ColumnMap {\n const { recipient, tenant, readAt, isRead } = resolveColumnMap(model?.columnMap);\n\n const columns: ColumnMap = {\n [recipient]: uuid().index().notNullable(),\n type: string().index().notNullable(),\n title: string().notNullable(),\n body: text().nullable(),\n payload: json().nullable(),\n };\n\n if (readAt) {\n columns[readAt] = timestamp().nullable();\n }\n\n if (isRead) {\n columns[isRead] = boolCol().default(false).index();\n }\n\n if (tenant) {\n columns[tenant] = uuid().index().notNullable();\n }\n\n // Unique so a retried send can't insert a duplicate. Nullable: most rows\n // have no key (NULLs are distinct in a unique index). `createFor` does\n // find-or-create; this constraint is the race backstop.\n columns.idempotency_key = string().nullable().unique();\n\n return columns;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,oBAAoB,OAA8C;CAChF,MAAM,EAAE,WAAW,QAAQ,QAAQ,WAAW,iBAAiB,OAAO,SAAS;CAE/E,MAAM,UAAqB;GACxB,YAAY,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,YAAY;EACxC,MAAM,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,YAAY;EACnC,OAAO,OAAO,CAAC,CAAC,YAAY;EAC5B,MAAM,KAAK,CAAC,CAAC,SAAS;EACtB,SAAS,KAAK,CAAC,CAAC,SAAS;CAC3B;CAEA,IAAI,QACF,QAAQ,UAAU,UAAU,CAAC,CAAC,SAAS;CAGzC,IAAI,QACF,QAAQ,UAAU,QAAQ,CAAC,CAAC,QAAQ,KAAK,CAAC,CAAC,MAAM;CAGnD,IAAI,QACF,QAAQ,UAAU,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,YAAY;CAM/C,QAAQ,kBAAkB,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO;CAErD,OAAO;AACT"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QueueDispatcher } from "../contracts/queue-dispatcher.contract.mjs";
|
|
2
|
-
//#region
|
|
2
|
+
//#region ../notifications/src/queue/herald-queue.d.ts
|
|
3
3
|
type HeraldQueueOptions = {
|
|
4
4
|
/** Herald channel to publish jobs to. Default `"notifications.dispatch"`. */channel?: string; /** Herald broker name (multi-broker setups). Default broker if omitted. */
|
|
5
5
|
broker?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"herald-queue.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"herald-queue.d.mts","names":[],"sources":["../../../../../../../notifications/src/queue/herald-queue.ts"],"mappings":";;KAyBY,kBAAA;+EAEV,OAAA;EAEA,MAAM;AAAA;AAAA,iBAGQ,WAAA,CAAY,OAAA,GAAS,kBAAA,GAA0B,eAAe"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_QUEUE_CHANNEL, loadHerald } from "./load-herald.mjs";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../notifications/src/queue/herald-queue.ts
|
|
4
4
|
function heraldQueue(options = {}) {
|
|
5
5
|
const channelName = options.channel ?? "notifications.dispatch";
|
|
6
6
|
return { async dispatch(job) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"herald-queue.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"herald-queue.mjs","names":[],"sources":["../../../../../../../notifications/src/queue/herald-queue.ts"],"sourcesContent":["/**\n * Herald-backed `QueueDispatcher` — the production backend for `.queue()`.\n *\n * `defineNotification` renders payloads + resolves routes BEFORE handing a job\n * to the dispatcher, so the job is fully serializable (`{ channel, route,\n * payload, options }`) — no model re-hydration or closure serialization. This\n * dispatcher just publishes the job onto a herald channel; the worker\n * (`startNotificationsWorker`) consumes it and runs `channel.send`.\n *\n * Connection is the app's job — the `@warlock.js/herald` connector connects the\n * broker from `config/herald.ts` at boot; this dispatcher only calls `herald()`.\n *\n * @example src/config/notifications.ts — declarative; the connector registers it\n * import { type NotificationConfig, heraldQueue, inApp, mailChannel } from \"@warlock.js/notifications\";\n *\n * const config: NotificationConfig = {\n * channels: { mail: mailChannel(), database: inApp.configure({ model: Notification }) },\n * queue: heraldQueue(), // → `.queue()` now works\n * };\n *\n * export default config;\n */\nimport type { QueueDispatcher } from \"../contracts\";\nimport { DEFAULT_QUEUE_CHANNEL, loadHerald } from \"./load-herald\";\n\nexport type HeraldQueueOptions = {\n /** Herald channel to publish jobs to. Default `\"notifications.dispatch\"`. */\n channel?: string;\n /** Herald broker name (multi-broker setups). Default broker if omitted. */\n broker?: string;\n};\n\nexport function heraldQueue(options: HeraldQueueOptions = {}): QueueDispatcher {\n const channelName = options.channel ?? DEFAULT_QUEUE_CHANNEL;\n\n return {\n async dispatch(job) {\n const { herald } = await loadHerald();\n await herald(options.broker).channel(channelName).publish(job);\n },\n };\n}\n"],"mappings":";;;AAgCA,SAAgB,YAAY,UAA8B,CAAC,GAAoB;CAC7E,MAAM,cAAc,QAAQ;CAE5B,OAAO,EACL,MAAM,SAAS,KAAK;EAClB,MAAM,EAAE,WAAW,MAAM,WAAW;EACpC,MAAM,OAAO,QAAQ,MAAM,CAAC,CAAC,QAAQ,WAAW,CAAC,CAAC,QAAQ,GAAG;CAC/D,EACF;AACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region ../notifications/src/queue/load-herald.d.ts
|
|
2
2
|
/** The herald channel notification jobs are published to / consumed from. */
|
|
3
3
|
declare const DEFAULT_QUEUE_CHANNEL = "notifications.dispatch";
|
|
4
4
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-herald.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"load-herald.d.mts","names":[],"sources":["../../../../../../../notifications/src/queue/load-herald.ts"],"mappings":";AAmCA;AAAA,cAAa,qBAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-herald.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"load-herald.mjs","names":[],"sources":["../../../../../../../notifications/src/queue/load-herald.ts"],"sourcesContent":["/**\n * Lazily load `@warlock.js/herald` — the optional peer backing `.queue()`.\n *\n * Memoized: the dynamic import runs at most once. A missing package surfaces a\n * curated install message at use time rather than a boot-time resolution error\n * (the lazy-optional-peer pattern used across the framework's drivers).\n */\nlet heraldModule: typeof import(\"@warlock.js/herald\") | undefined;\n\nconst INSTALL_INSTRUCTIONS = `\nThe notifications queue requires the @warlock.js/herald package.\nInstall it with:\n\n npm install @warlock.js/herald\n\nOr with your preferred package manager:\n\n pnpm add @warlock.js/herald\n yarn add @warlock.js/herald\n`.trim();\n\nexport async function loadHerald(): Promise<typeof import(\"@warlock.js/herald\")> {\n if (heraldModule) {\n return heraldModule;\n }\n\n try {\n heraldModule = await import(\"@warlock.js/herald\");\n return heraldModule;\n } catch {\n throw new Error(INSTALL_INSTRUCTIONS);\n }\n}\n\n/** The herald channel notification jobs are published to / consumed from. */\nexport const DEFAULT_QUEUE_CHANNEL = \"notifications.dispatch\";\n"],"mappings":";;;;;;;;AAOA,IAAI;AAEJ,MAAM,uBAAuB;;;;;;;;;;EAU3B,KAAK;AAEP,eAAsB,aAA2D;CAC/E,IAAI,cACF,OAAO;CAGT,IAAI;EACF,eAAe,MAAM,OAAO;EAC5B,OAAO;CACT,QAAQ;EACN,MAAM,IAAI,MAAM,oBAAoB;CACtC;AACF;;AAGA,MAAa,wBAAwB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region
|
|
1
|
+
//#region ../notifications/src/queue/notifications-worker.d.ts
|
|
2
2
|
type WorkerOptions = {
|
|
3
3
|
/** Herald channel to consume from. Must match the dispatcher. Default `"notifications.dispatch"`. */channel?: string; /** Herald broker name. Default broker if omitted. */
|
|
4
4
|
broker?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifications-worker.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"notifications-worker.d.mts","names":[],"sources":["../../../../../../../notifications/src/queue/notifications-worker.ts"],"mappings":";KAyBY,aAAA;EAAA,qGAEV,OAAA;EAEA,MAAM;AAAA;AAAA,iBAGc,wBAAA,CAAyB,OAAA,GAAS,aAAA,GAAqB,OAAO"}
|
|
@@ -2,7 +2,7 @@ import { getNotificationConfig } from "../config.mjs";
|
|
|
2
2
|
import { DEFAULT_QUEUE_CHANNEL, loadHerald } from "./load-herald.mjs";
|
|
3
3
|
import { log } from "@warlock.js/logger";
|
|
4
4
|
|
|
5
|
-
//#region
|
|
5
|
+
//#region ../notifications/src/queue/notifications-worker.ts
|
|
6
6
|
/**
|
|
7
7
|
* Notification queue worker — consumes the jobs `heraldQueue` publishes and
|
|
8
8
|
* runs the actual `channel.send`. Call this once in a worker process (or the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifications-worker.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"notifications-worker.mjs","names":[],"sources":["../../../../../../../notifications/src/queue/notifications-worker.ts"],"sourcesContent":["/**\n * Notification queue worker — consumes the jobs `heraldQueue` publishes and\n * runs the actual `channel.send`. Call this once in a worker process (or the\n * web process) after the notifications config + herald broker are up.\n *\n * The job carries an ALREADY-RENDERED payload + resolved route, so the worker\n * only looks the channel up by name and dispatches — no notifiable needed.\n *\n * @example\n * // in a worker entrypoint, after the notifications config + broker are up\n * import { startNotificationsWorker } from \"@warlock.js/notifications\";\n * await startNotificationsWorker();\n */\nimport { log } from \"@warlock.js/logger\";\nimport { getNotificationConfig } from \"../config\";\nimport type { ChannelName, SendOptions } from \"../types\";\nimport { DEFAULT_QUEUE_CHANNEL, loadHerald } from \"./load-herald\";\n\ntype NotificationJob = {\n channel: string;\n route: unknown;\n payload: unknown;\n options: SendOptions;\n};\n\nexport type WorkerOptions = {\n /** Herald channel to consume from. Must match the dispatcher. Default `\"notifications.dispatch\"`. */\n channel?: string;\n /** Herald broker name. Default broker if omitted. */\n broker?: string;\n};\n\nexport async function startNotificationsWorker(options: WorkerOptions = {}): Promise<void> {\n const channelName = options.channel ?? DEFAULT_QUEUE_CHANNEL;\n const { herald } = await loadHerald();\n\n await herald(options.broker)\n .channel(channelName)\n .subscribe(async (message: { payload: unknown }, ctx: { ack(): Promise<void> }) => {\n const job = message.payload as NotificationJob;\n\n try {\n const channel = getNotificationConfig().channels[job.channel as ChannelName];\n\n if (!channel) {\n // Unknown channel — log and ack to avoid a poison-message loop.\n log.error(\"notifications\", \"queue.worker\", `Unknown channel \"${job.channel}\" — dropping job`);\n await ctx.ack();\n return;\n }\n\n await channel.send({\n payload: job.payload as never,\n route: job.route as never,\n options: job.options,\n });\n\n await ctx.ack();\n } catch (error) {\n // Phase 2: log + ack (no retry/DLQ yet — that lands with the\n // delay-aware worker). A dead-letter strategy is a follow-up.\n log.error(\"notifications\", \"queue.worker\", error as Error);\n await ctx.ack();\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgCA,eAAsB,yBAAyB,UAAyB,CAAC,GAAkB;CACzF,MAAM,cAAc,QAAQ;CAC5B,MAAM,EAAE,WAAW,MAAM,WAAW;CAEpC,MAAM,OAAO,QAAQ,MAAM,CAAC,CACzB,QAAQ,WAAW,CAAC,CACpB,UAAU,OAAO,SAA+B,QAAkC;EACjF,MAAM,MAAM,QAAQ;EAEpB,IAAI;GACF,MAAM,UAAU,sBAAsB,CAAC,CAAC,SAAS,IAAI;GAErD,IAAI,CAAC,SAAS;IAEZ,IAAI,MAAM,iBAAiB,gBAAgB,oBAAoB,IAAI,QAAQ,iBAAiB;IAC5F,MAAM,IAAI,IAAI;IACd;GACF;GAEA,MAAM,QAAQ,KAAK;IACjB,SAAS,IAAI;IACb,OAAO,IAAI;IACX,SAAS,IAAI;GACf,CAAC;GAED,MAAM,IAAI,IAAI;EAChB,SAAS,OAAO;GAGd,IAAI,MAAM,iBAAiB,gBAAgB,KAAc;GACzD,MAAM,IAAI,IAAI;EAChB;CACF,CAAC;AACL"}
|
package/esm/types.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Model } from "@warlock.js/cascade";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region ../notifications/src/types.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* A recipient you send TO (a User, etc.) — NOT the stored record.
|
|
6
6
|
* It's a cascade `Model` instance, so `.id` (number | string) and `.get(path)`
|
package/esm/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../notifications/src/types.ts"],"mappings":";;;AAqBA;;;;AAAc;AAKd;;AALA,KAHY,UAAA,GAAa,KAAK;;KAGlB,EAAA;;KAKA,WAAA;EACV,OAAA;EACA,IAAA;EACA,IAAA;EACA,EAAA;AAAA;;KAIU,iBAAA;EACV,WAAA,EAAa,EAAA;EACb,IAAA;EACA,KAAA;EACA,IAAA;EACA,OAAA,GAAU,MAAM,mBAAhB;EAEA,cAAA;AAAA;;AAAc;AAOhB;;KAAY,eAAA,GAAkB,IAAI,CAAC,iBAAA;;AAAiB;AAiBpD;;;;;;;;;AAE2B;AAG3B;UALiB,oBAAA;EACf,IAAA,EAAM,WAAA;EACN,QAAA,EAAU,eAAe;AAAA;AAAA,KAGf,WAAA,SAAoB,oBAAoB;;;;;;;;;;;;AAkC9C;KAlBM,WAAA;EAsBa;;;AAAmB;AAiB5C;;;EA/BE,KAAA,oBAqCW;EAnCX,MAAA,WA0CW;EAxCX,IAAA,GAAO,MAAM,mBAgDJ;EA9CT,cAAA,WAqDe;EAnDf,KAAA,YAqDsB;EAnDtB,IAAA;AAAA;;KAIU,aAAA,GAAgB,IAAI,CAAC,WAAA;;;;;;;;;;;;;;KAiBrB,kBAAA;EACV,OAAA;IACE,UAAA;IACA,OAAA;IACA,UAAA,GAAa,UAAA;IACb,OAAA;IACA,OAAA,EAAS,WAAA;EAAA;EAEX,IAAA;IACE,UAAA;IACA,OAAA;IACA,UAAA,GAAa,UAAA;IACb,OAAA;IACA,OAAA,EAAS,WAAA;IACT,UAAA;EAAA;EAEF,MAAA;IACE,UAAA;IACA,OAAA;IACA,UAAA,GAAa,UAAA;IACb,OAAA;IACA,KAAA,EAAO,KAAA;IACP,OAAA,EAAS,WAAA;IACT,UAAA;EAAA;EAEF,OAAA;IACE,UAAA;IACA,OAAA;IACA,UAAA,GAAa,UAAA;IACb,MAAA;IACA,OAAA,EAAS,WAAA;EAAA;AAAA"}
|
package/package.json
CHANGED
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
"url": "https://github.com/warlockjs/notifications"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@warlock.js/cache": "4.
|
|
20
|
-
"@warlock.js/cascade": "4.
|
|
21
|
-
"@warlock.js/core": "4.
|
|
22
|
-
"@warlock.js/logger": "4.
|
|
19
|
+
"@warlock.js/cache": "4.9.1",
|
|
20
|
+
"@warlock.js/cascade": "4.9.1",
|
|
21
|
+
"@warlock.js/core": "4.9.1",
|
|
22
|
+
"@warlock.js/logger": "4.9.1"
|
|
23
23
|
},
|
|
24
|
-
"version": "4.
|
|
24
|
+
"version": "4.9.1",
|
|
25
25
|
"main": "./cjs/index.cjs",
|
|
26
26
|
"module": "./esm/index.mjs",
|
|
27
27
|
"types": "./esm/index.d.mts",
|