@warlock.js/notifications 5.45.0 → 5.47.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 +14 -0
- package/cjs/index.cjs +112 -32
- package/cjs/index.cjs.map +1 -1
- package/esm/channels/database-channel.d.mts.map +1 -1
- package/esm/channels/database-channel.mjs +10 -2
- package/esm/channels/database-channel.mjs.map +1 -1
- package/esm/contracts/channel.contract.d.mts +3 -1
- package/esm/contracts/channel.contract.d.mts.map +1 -1
- package/esm/errors.d.mts +9 -0
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +14 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/in-app/base-notifications-repository.d.mts +21 -7
- package/esm/in-app/base-notifications-repository.d.mts.map +1 -1
- package/esm/in-app/base-notifications-repository.mjs +41 -19
- package/esm/in-app/base-notifications-repository.mjs.map +1 -1
- package/esm/in-app/column-map.d.mts +8 -0
- package/esm/in-app/column-map.d.mts.map +1 -1
- package/esm/in-app/column-map.mjs +3 -0
- package/esm/in-app/column-map.mjs.map +1 -1
- package/esm/in-app/in-app.d.mts +9 -8
- package/esm/in-app/in-app.d.mts.map +1 -1
- package/esm/in-app/in-app.mjs +7 -8
- package/esm/in-app/in-app.mjs.map +1 -1
- package/esm/in-app/index.d.mts +2 -1
- package/esm/in-app/index.mjs +2 -1
- package/esm/in-app/recipient.d.mts +17 -0
- package/esm/in-app/recipient.d.mts.map +1 -0
- package/esm/in-app/recipient.mjs +27 -0
- package/esm/in-app/recipient.mjs.map +1 -0
- package/esm/index.d.mts +4 -3
- package/esm/index.mjs +3 -2
- package/esm/migration/notification-columns.d.mts +12 -1
- package/esm/migration/notification-columns.d.mts.map +1 -1
- package/esm/migration/notification-columns.mjs +13 -3
- package/esm/migration/notification-columns.mjs.map +1 -1
- package/esm/types.d.mts +13 -1
- package/esm/types.d.mts.map +1 -1
- package/llms-full.txt +11 -4
- package/llms.txt +1 -1
- package/package.json +7 -7
- package/skills/use-in-app/SKILL.md +6 -1
- package/skills/write-notification-migration/SKILL.md +5 -3
package/esm/index.d.mts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { ChannelName, DatabasePayload, Id, MailPayload, Notifiable, NotificationChannels, NotificationEvents, NotificationInput, RenderContext, SendOptions } from "./types.mjs";
|
|
1
|
+
import { ChannelName, DatabasePayload, Id, MailPayload, Notifiable, NotificationChannels, NotificationEvents, NotificationInput, RecipientRef, RenderContext, SendOptions } from "./types.mjs";
|
|
2
2
|
import { Channel } from "./contracts/channel.contract.mjs";
|
|
3
3
|
import { NotificationContract } from "./contracts/notification.contract.mjs";
|
|
4
4
|
import { PreferenceProvider } from "./contracts/preference-provider.contract.mjs";
|
|
5
5
|
import { QueueDispatcher } from "./contracts/queue-dispatcher.contract.mjs";
|
|
6
6
|
import { RateLimiter } from "./contracts/rate-limiter.contract.mjs";
|
|
7
7
|
import "./contracts/index.mjs";
|
|
8
|
-
import { ChannelNotFoundError, MissingRendererError, NoQueueDispatcherError, NotificationsNotConfiguredError, UnresolvableRouteError } from "./errors.mjs";
|
|
8
|
+
import { ChannelNotFoundError, MissingRendererError, NoQueueDispatcherError, NotificationsNotConfiguredError, RecipientTypeRequiredError, UnresolvableRouteError } from "./errors.mjs";
|
|
9
9
|
import { ChannelMap, NotificationConfig, getNotificationConfig, resetNotificationConfig, setNotificationConfig } from "./config.mjs";
|
|
10
10
|
import { defineChannel } from "./dispatch/define-channel.mjs";
|
|
11
11
|
import { DefinedNotification, NotificationDef, defineNotification } from "./dispatch/define-notification.mjs";
|
|
@@ -14,6 +14,7 @@ import { notifications } from "./dispatch/notifications-event-bus.mjs";
|
|
|
14
14
|
import "./dispatch/index.mjs";
|
|
15
15
|
import { NotificationColumnMap, NotificationColumnMapHost, ResolvedNotificationColumnMap, resolveColumnMap } from "./in-app/column-map.mjs";
|
|
16
16
|
import { DatabaseNotification } from "./in-app/database-notification.mjs";
|
|
17
|
+
import { RecipientInput, recipientTypeOf, toRecipientRef } from "./in-app/recipient.mjs";
|
|
17
18
|
import { BaseNotificationsRepository, NotificationModelClass, NotificationsFilter } from "./in-app/base-notifications-repository.mjs";
|
|
18
19
|
import { databaseChannel } from "./channels/database-channel.mjs";
|
|
19
20
|
import { MailChannelConfig, mailChannel } from "./channels/mail-channel.mjs";
|
|
@@ -29,6 +30,6 @@ import { BullmqQueueOptions, NOTIFICATION_JOB_NAME, bullmqQueue } from "./queue/
|
|
|
29
30
|
import { QueuePackageLoader, loadQueuePackage } from "./queue/load-queue.mjs";
|
|
30
31
|
import { QueuePackageNotInstalledError } from "./queue/queue-package-not-installed.error.mjs";
|
|
31
32
|
import "./queue/index.mjs";
|
|
32
|
-
export { BaseNotificationsRepository, BullmqQueueOptions, Channel, ChannelMap, ChannelName, ChannelNotFoundError, ConfigureOptions, DEFAULT_QUEUE_CHANNEL, DatabaseNotification, DatabasePayload, DefinedNotification, HeraldQueueOptions, Id, MailChannelConfig, MailPayload, MissingRendererError, NOTIFICATION_JOB_NAME, NoQueueDispatcherError, Notifiable, NotificationChannels, NotificationColumnMap, NotificationColumnMapHost, NotificationColumnsOptions, NotificationConfig, NotificationContract, NotificationDef, NotificationEvents, NotificationIdType, NotificationInput, NotificationModelClass, NotificationsFilter, NotificationsListOptions, NotificationsNotConfiguredError, Notify, PreferenceProvider, QueueDispatcher, type QueuePackageLoader, QueuePackageNotInstalledError, RateLimiter, RenderContext, ResolvedNotificationColumnMap, SendOptions, UnresolvableRouteError, WorkerOptions, bullmqQueue, databaseChannel, defineChannel, defineNotification, getNotificationConfig, heraldQueue, inApp, loadQueuePackage, mailChannel, notificationColumns, notifications, notify, resetNotificationConfig, resolveColumnMap, setNotificationConfig, startNotificationsWorker };
|
|
33
|
+
export { BaseNotificationsRepository, BullmqQueueOptions, Channel, ChannelMap, ChannelName, ChannelNotFoundError, ConfigureOptions, DEFAULT_QUEUE_CHANNEL, DatabaseNotification, DatabasePayload, DefinedNotification, HeraldQueueOptions, Id, MailChannelConfig, MailPayload, MissingRendererError, NOTIFICATION_JOB_NAME, NoQueueDispatcherError, Notifiable, NotificationChannels, NotificationColumnMap, NotificationColumnMapHost, NotificationColumnsOptions, NotificationConfig, NotificationContract, NotificationDef, NotificationEvents, NotificationIdType, NotificationInput, NotificationModelClass, NotificationsFilter, NotificationsListOptions, NotificationsNotConfiguredError, Notify, PreferenceProvider, QueueDispatcher, type QueuePackageLoader, QueuePackageNotInstalledError, RateLimiter, RecipientInput, RecipientRef, RecipientTypeRequiredError, RenderContext, ResolvedNotificationColumnMap, SendOptions, UnresolvableRouteError, WorkerOptions, bullmqQueue, databaseChannel, defineChannel, defineNotification, getNotificationConfig, heraldQueue, inApp, loadQueuePackage, mailChannel, notificationColumns, notifications, notify, recipientTypeOf, resetNotificationConfig, resolveColumnMap, setNotificationConfig, startNotificationsWorker, toRecipientRef };
|
|
33
34
|
import "./dispatch/define-channel.mjs";
|
|
34
35
|
import "./types.mjs";
|
package/esm/index.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { ChannelNotFoundError, MissingRendererError, NoQueueDispatcherError, NotificationsNotConfiguredError, UnresolvableRouteError } from "./errors.mjs";
|
|
1
|
+
import { ChannelNotFoundError, MissingRendererError, NoQueueDispatcherError, NotificationsNotConfiguredError, RecipientTypeRequiredError, UnresolvableRouteError } from "./errors.mjs";
|
|
2
2
|
import { getNotificationConfig, resetNotificationConfig, setNotificationConfig } from "./config.mjs";
|
|
3
3
|
import { defineChannel } from "./dispatch/define-channel.mjs";
|
|
4
4
|
import { notifications } from "./dispatch/notifications-event-bus.mjs";
|
|
5
5
|
import { defineNotification } from "./dispatch/define-notification.mjs";
|
|
6
6
|
import { notify } from "./dispatch/notify.mjs";
|
|
7
7
|
import "./dispatch/index.mjs";
|
|
8
|
+
import { recipientTypeOf, toRecipientRef } from "./in-app/recipient.mjs";
|
|
8
9
|
import { databaseChannel } from "./channels/database-channel.mjs";
|
|
9
10
|
import { mailChannel } from "./channels/mail-channel.mjs";
|
|
10
11
|
import "./channels/index.mjs";
|
|
@@ -23,4 +24,4 @@ import { loadQueuePackage } from "./queue/load-queue.mjs";
|
|
|
23
24
|
import { NOTIFICATION_JOB_NAME, bullmqQueue } from "./queue/bullmq-queue.mjs";
|
|
24
25
|
import "./queue/index.mjs";
|
|
25
26
|
|
|
26
|
-
export { BaseNotificationsRepository, ChannelNotFoundError, DEFAULT_QUEUE_CHANNEL, DatabaseNotification, MissingRendererError, NOTIFICATION_JOB_NAME, NoQueueDispatcherError, NotificationsNotConfiguredError, QueuePackageNotInstalledError, UnresolvableRouteError, bullmqQueue, databaseChannel, defineChannel, defineNotification, getNotificationConfig, heraldQueue, inApp, loadQueuePackage, mailChannel, notificationColumns, notifications, notify, resetNotificationConfig, resolveColumnMap, setNotificationConfig, startNotificationsWorker };
|
|
27
|
+
export { BaseNotificationsRepository, ChannelNotFoundError, DEFAULT_QUEUE_CHANNEL, DatabaseNotification, MissingRendererError, NOTIFICATION_JOB_NAME, NoQueueDispatcherError, NotificationsNotConfiguredError, QueuePackageNotInstalledError, RecipientTypeRequiredError, UnresolvableRouteError, bullmqQueue, databaseChannel, defineChannel, defineNotification, getNotificationConfig, heraldQueue, inApp, loadQueuePackage, mailChannel, notificationColumns, notifications, notify, recipientTypeOf, resetNotificationConfig, resolveColumnMap, setNotificationConfig, startNotificationsWorker, toRecipientRef };
|
|
@@ -3,8 +3,19 @@ import { ColumnMap } from "@warlock.js/cascade";
|
|
|
3
3
|
//#region ../../packages/notifications/src/migration/notification-columns.d.ts
|
|
4
4
|
export type NotificationIdType = "uuid" | "bigint" | "integer" | "string";
|
|
5
5
|
export type NotificationColumnsOptions = {
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* The type used for recipient and tenant identifiers. Defaults to the id
|
|
8
|
+
* type derived from `recipientModel` when given, otherwise UUID.
|
|
9
|
+
*/
|
|
7
10
|
idType?: NotificationIdType;
|
|
11
|
+
/**
|
|
12
|
+
* The recipient model (a `User`, `Customer`, …). Auto-generated ids are
|
|
13
|
+
* integers, so the id columns are `integer`; a model with
|
|
14
|
+
* `autoGenerateId = false` brings its own ids, so they are `uuid`.
|
|
15
|
+
*/
|
|
16
|
+
recipientModel?: {
|
|
17
|
+
autoGenerateId?: boolean;
|
|
18
|
+
};
|
|
8
19
|
};
|
|
9
20
|
/**
|
|
10
21
|
* Returns the column map for a notification table, named from the model's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-columns.d.mts","names":[],"sources":["../../../../../../../../packages/notifications/src/migration/notification-columns.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"notification-columns.d.mts","names":[],"sources":["../../../../../../../../packages/notifications/src/migration/notification-columns.ts"],"mappings":";;;YA0CY;YAEA;;;;;EAKV,SAAS;;;;;;EAMT;IAAmB;;;;;;;;wBAQL,oBAAoB,UAAU,6BAA6B;wBAC3D,oBACd,QAAQ,2BACR,UAAU,6BACT"}
|
|
@@ -9,6 +9,10 @@ import { bigInteger, boolCol, integer, json, string, text, timestamp, uuid } fro
|
|
|
9
9
|
* agree by construction. The stable lexical columns (`type`, `title`, `body`,
|
|
10
10
|
* `payload`, `idempotency_key`) keep fixed names.
|
|
11
11
|
*
|
|
12
|
+
* `columnMap.recipientType` adds the recipient-type string column (see
|
|
13
|
+
* `NotificationColumnMap`). For a table created earlier, add that column with a
|
|
14
|
+
* migration instead of re-creating the table.
|
|
15
|
+
*
|
|
12
16
|
* Read-state follows the map's presence rules (see `NotificationColumnMap`):
|
|
13
17
|
* `readAt` → a nullable timestamp, `isRead` → an indexed boolean, both → both.
|
|
14
18
|
*
|
|
@@ -29,8 +33,9 @@ import { bigInteger, boolCol, integer, json, string, text, timestamp, uuid } fro
|
|
|
29
33
|
*/
|
|
30
34
|
function notificationColumns(modelOrOptions, options) {
|
|
31
35
|
const model = isNotificationColumnsOptions(modelOrOptions) ? void 0 : modelOrOptions;
|
|
32
|
-
const
|
|
33
|
-
const
|
|
36
|
+
const resolvedOptions = isNotificationColumnsOptions(modelOrOptions) ? modelOrOptions : options;
|
|
37
|
+
const idType = resolvedOptions?.idType ?? deriveIdType(resolvedOptions?.recipientModel);
|
|
38
|
+
const { recipient, recipientType, tenant, readAt, isRead } = resolveColumnMap(model?.columnMap);
|
|
34
39
|
const idColumn = getIdColumn(idType);
|
|
35
40
|
const columns = {
|
|
36
41
|
[recipient]: idColumn().index().notNullable(),
|
|
@@ -39,6 +44,7 @@ function notificationColumns(modelOrOptions, options) {
|
|
|
39
44
|
body: text().nullable(),
|
|
40
45
|
payload: json().nullable()
|
|
41
46
|
};
|
|
47
|
+
if (recipientType) columns[recipientType] = string().index().notNullable();
|
|
42
48
|
if (readAt) columns[readAt] = timestamp().nullable();
|
|
43
49
|
if (isRead) columns[isRead] = boolCol().default(false).index();
|
|
44
50
|
if (tenant) columns[tenant] = idColumn().index().notNullable();
|
|
@@ -46,7 +52,7 @@ function notificationColumns(modelOrOptions, options) {
|
|
|
46
52
|
return columns;
|
|
47
53
|
}
|
|
48
54
|
function isNotificationColumnsOptions(value) {
|
|
49
|
-
return "idType" in (value ?? {});
|
|
55
|
+
return "idType" in (value ?? {}) || "recipientModel" in (value ?? {});
|
|
50
56
|
}
|
|
51
57
|
function getIdColumn(idType) {
|
|
52
58
|
switch (idType) {
|
|
@@ -56,6 +62,10 @@ function getIdColumn(idType) {
|
|
|
56
62
|
default: return uuid;
|
|
57
63
|
}
|
|
58
64
|
}
|
|
65
|
+
function deriveIdType(recipientModel) {
|
|
66
|
+
if (!recipientModel || recipientModel.autoGenerateId === false) return "uuid";
|
|
67
|
+
return "integer";
|
|
68
|
+
}
|
|
59
69
|
|
|
60
70
|
//#endregion
|
|
61
71
|
export { notificationColumns };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-columns.mjs","names":[],"sources":["../../../../../../../../packages/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 {\n bigInteger,\n boolCol,\n type ColumnMap,\n integer,\n json,\n string,\n text,\n timestamp,\n uuid,\n} from \"@warlock.js/cascade\";\nimport { type NotificationColumnMapHost, resolveColumnMap } from \"../in-app/column-map\";\n\nexport type NotificationIdType = \"uuid\" | \"bigint\" | \"integer\" | \"string\";\n\nexport type NotificationColumnsOptions = {\n
|
|
1
|
+
{"version":3,"file":"notification-columns.mjs","names":[],"sources":["../../../../../../../../packages/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 * `columnMap.recipientType` adds the recipient-type string column (see\n * `NotificationColumnMap`). For a table created earlier, add that column with a\n * migration instead of re-creating the table.\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 {\n bigInteger,\n boolCol,\n type ColumnMap,\n integer,\n json,\n string,\n text,\n timestamp,\n uuid,\n} from \"@warlock.js/cascade\";\nimport { type NotificationColumnMapHost, resolveColumnMap } from \"../in-app/column-map\";\n\nexport type NotificationIdType = \"uuid\" | \"bigint\" | \"integer\" | \"string\";\n\nexport type NotificationColumnsOptions = {\n /**\n * The type used for recipient and tenant identifiers. Defaults to the id\n * type derived from `recipientModel` when given, otherwise UUID.\n */\n idType?: NotificationIdType;\n /**\n * The recipient model (a `User`, `Customer`, …). Auto-generated ids are\n * integers, so the id columns are `integer`; a model with\n * `autoGenerateId = false` brings its own ids, so they are `uuid`.\n */\n recipientModel?: { autoGenerateId?: boolean };\n};\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(options?: NotificationColumnsOptions): ColumnMap;\nexport function notificationColumns(\n model?: NotificationColumnMapHost,\n options?: NotificationColumnsOptions,\n): ColumnMap;\nexport function notificationColumns(\n modelOrOptions?: NotificationColumnMapHost | NotificationColumnsOptions,\n options?: NotificationColumnsOptions,\n): ColumnMap {\n const model = isNotificationColumnsOptions(modelOrOptions) ? undefined : modelOrOptions;\n const resolvedOptions = isNotificationColumnsOptions(modelOrOptions) ? modelOrOptions : options;\n const idType = resolvedOptions?.idType ?? deriveIdType(resolvedOptions?.recipientModel);\n const { recipient, recipientType, tenant, readAt, isRead } = resolveColumnMap(model?.columnMap);\n const idColumn = getIdColumn(idType);\n\n const columns: ColumnMap = {\n [recipient]: idColumn().index().notNullable(),\n type: string().index().notNullable(),\n title: string().notNullable(),\n body: text().nullable(),\n payload: json().nullable(),\n };\n\n if (recipientType) {\n // Indexed together with the recipient id by the app's own composite index when needed.\n columns[recipientType] = string().index().notNullable();\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] = idColumn().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\nfunction isNotificationColumnsOptions(\n value: NotificationColumnMapHost | NotificationColumnsOptions | undefined,\n): value is NotificationColumnsOptions {\n return \"idType\" in (value ?? {}) || \"recipientModel\" in (value ?? {});\n}\n\nfunction getIdColumn(idType: NotificationIdType) {\n switch (idType) {\n case \"bigint\":\n return bigInteger;\n case \"integer\":\n return integer;\n case \"string\":\n return string;\n default:\n return uuid;\n }\n}\n\nfunction deriveIdType(recipientModel?: { autoGenerateId?: boolean }): NotificationIdType {\n if (!recipientModel || recipientModel.autoGenerateId === false) {\n return \"uuid\";\n }\n\n return \"integer\";\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoEA,SAAgB,oBACd,gBACA,SACW;CACX,MAAM,QAAQ,6BAA6B,cAAc,IAAI,SAAY;CACzE,MAAM,kBAAkB,6BAA6B,cAAc,IAAI,iBAAiB;CACxF,MAAM,SAAS,iBAAiB,UAAU,aAAa,iBAAiB,cAAc;CACtF,MAAM,EAAE,WAAW,eAAe,QAAQ,QAAQ,WAAW,iBAAiB,OAAO,SAAS;CAC9F,MAAM,WAAW,YAAY,MAAM;CAEnC,MAAM,UAAqB;GACxB,YAAY,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,YAAY;EAC5C,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,eAEF,QAAQ,iBAAiB,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,YAAY;CAGxD,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,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,YAAY;CAMnD,QAAQ,kBAAkB,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO;CAErD,OAAO;AACT;AAEA,SAAS,6BACP,OACqC;CACrC,OAAO,aAAa,SAAS,CAAC,MAAM,qBAAqB,SAAS,CAAC;AACrE;AAEA,SAAS,YAAY,QAA4B;CAC/C,QAAQ,QAAR;EACE,KAAK,UACH,OAAO;EACT,KAAK,WACH,OAAO;EACT,KAAK,UACH,OAAO;EACT,SACE,OAAO;CACX;AACF;AAEA,SAAS,aAAa,gBAAmE;CACvF,IAAI,CAAC,kBAAkB,eAAe,mBAAmB,OACvD,OAAO;CAGT,OAAO;AACT"}
|
package/esm/types.d.mts
CHANGED
|
@@ -10,6 +10,16 @@ import { Model } from "@warlock.js/cascade";
|
|
|
10
10
|
export type Notifiable = Model;
|
|
11
11
|
/** An identifier — string (uuid / objectId) or number (auto-increment). */
|
|
12
12
|
export type Id = string | number;
|
|
13
|
+
/**
|
|
14
|
+
* A recipient named without its model: the identity `type` (an auth model's
|
|
15
|
+
* `userType`, otherwise its table name) plus its `id`. Use it where only the
|
|
16
|
+
* id is at hand but the notifications table keys recipients by type. A ref
|
|
17
|
+
* without `type` is treated like a bare id.
|
|
18
|
+
*/
|
|
19
|
+
export type RecipientRef = {
|
|
20
|
+
type?: string;
|
|
21
|
+
id: Id;
|
|
22
|
+
};
|
|
13
23
|
/** What the `mail` channel renders to and `core.sendMail` consumes. */
|
|
14
24
|
export type MailPayload = {
|
|
15
25
|
subject: string;
|
|
@@ -20,6 +30,8 @@ export type MailPayload = {
|
|
|
20
30
|
/** Normalized in-app record shape written through the database channel. */
|
|
21
31
|
export type NotificationInput = {
|
|
22
32
|
recipientId: Id;
|
|
33
|
+
/** Identity type of the recipient — set by the channel from the recipient model. */
|
|
34
|
+
recipientType?: string;
|
|
23
35
|
type: string;
|
|
24
36
|
title: string;
|
|
25
37
|
body?: string;
|
|
@@ -31,7 +43,7 @@ export type NotificationInput = {
|
|
|
31
43
|
* Database payload as seen by RENDERERS — `recipientId` is set by the channel
|
|
32
44
|
* from the resolved route, not by the render closure.
|
|
33
45
|
*/
|
|
34
|
-
export type DatabasePayload = Omit<NotificationInput, "recipientId">;
|
|
46
|
+
export type DatabasePayload = Omit<NotificationInput, "recipientId" | "recipientType">;
|
|
35
47
|
/**
|
|
36
48
|
* Channel REGISTRY — the single declaration-merge target mapping channel NAME
|
|
37
49
|
* → payload type. Drives `notify.<channel>` proxy typing and
|
package/esm/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../packages/notifications/src/types.ts"],"mappings":";;;;;;;;;YAkBY,aAAa;;YAGb
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../packages/notifications/src/types.ts"],"mappings":";;;;;;;;;YAkBY,aAAa;;YAGb;;;;;;;YAQA;EAAiB;EAAe,IAAI;;;YAKpC;EACV;EACA;EACA;EACA;;;YAIU;EACV,aAAa;;EAEb;EACA;EACA;EACA;EACA,UAAU;;EAEV;;;;;;YAOU,kBAAkB,KAAK;;;;;;;;;;;;;;iBAiBlB;EACf,MAAM;EACN,UAAU;;YAGA,oBAAoB;;;;;;;;;;;;;YAgBpB;;;;;;;;EAQV;;EAEA;;EAEA,OAAO;;EAEP;;EAEA;;EAEA;;;YAIU,gBAAgB,KAAK;;;;;;;;;;;;;;YAiBrB;EACV;IACE;IACA;IACA,aAAa;IACb;IACA,SAAS;;EAEX;IACE;IACA;IACA,aAAa;IACb;IACA,SAAS;IACT;;EAEF;IACE;IACA;IACA,aAAa;IACb;IACA,OAAO;IACP,SAAS;IACT;;EAEF;IACE;IACA;IACA,aAAa;IACb;IACA,SAAS"}
|
package/llms-full.txt
CHANGED
|
@@ -903,13 +903,18 @@ class BaseNotificationsRepository extends RepositoryManager {
|
|
|
903
903
|
|
|
904
904
|
### `createFor` write side — no IDOR either (since 4.15.0)
|
|
905
905
|
|
|
906
|
-
`recipientId`
|
|
906
|
+
`recipientId` (and `recipientType`) are always the trusted arguments, applied **after** the payload — `createFor`/`createManyFor` strip server-owned keys (`id`, `recipientId`, `recipientType`, `tenant`, `readAt`, `isRead`, and their resolved `columnMap` physical column names) off `input` before merging. Before this, a caller passing an untyped payload straight through (request JSON, a channel's `send` args) could smuggle its own `recipientId` — or the raw physical column name — and write the notification into someone else's inbox. `type` / `title` / `body` / `payload` / `idempotencyKey` are untouched; you don't need to sanitize `input` yourself.
|
|
907
|
+
|
|
908
|
+
### Recipient types — identities that share an id
|
|
909
|
+
|
|
910
|
+
Recipients are keyed by `(type, id)`. Declare `recipientType: "recipient_type"` in `columnMap` and every `inApp` read / count / mark / dismiss filters on both, so staff `5` and customer `5` never see each other's rows. The type is the recipient model's `userType` (auth models) or its table name; the `database` channel stores it. Also accepted: `{ type, id }` (`RecipientRef`). `recipientTypeOf(model)` returns the type a model is stored under, and `toRecipientRef(recipient)` turns a model or ref into `{ type, id }`. With the column declared a **bare id throws `RecipientTypeRequiredError`**; without it ids are the only key (single-identity apps, tables created before 5.46.0). Upgrading: add a nullable `recipient_type` column, backfill it (unbackfilled rows match no typed read), then declare it in `columnMap`. Full steps in the docs page "The in-app store".
|
|
907
911
|
|
|
908
912
|
### `columnMap` — map logical roles → your columns
|
|
909
913
|
|
|
910
914
|
```ts
|
|
911
915
|
export type NotificationColumnMap = {
|
|
912
916
|
recipient?: string; // recipient FK. Default "user_id"
|
|
917
|
+
recipientType?: string; // recipient identity type column. Omit → ids are the only key
|
|
913
918
|
tenant?: string; // multi-tenant scope, written from the recipient. Omit → single-tenant
|
|
914
919
|
readAt?: string; // read-timestamp column
|
|
915
920
|
isRead?: string; // read-flag column (indexed)
|
|
@@ -992,7 +997,7 @@ database: inApp.configure({ repository: notificationsRepository }),
|
|
|
992
997
|
|
|
993
998
|
---
|
|
994
999
|
name: write-notification-migration
|
|
995
|
-
description: 'Create the notifications table with the `notificationColumns(Notification)` factory + cascade `Migration.create`. Column NAMES come from the model''s `columnMap` (recipient / tenant / readAt / isRead); the fixed columns are `type` / `title` / `body` / `payload` / `idempotency_key`.
|
|
1000
|
+
description: 'Create the notifications table with the `notificationColumns(Notification)` factory + cascade `Migration.create`. Column NAMES come from the model''s `columnMap` (recipient / tenant / readAt / isRead); the fixed columns are `type` / `title` / `body` / `payload` / `idempotency_key`. Id columns default to UUID; pass `{ recipientModel: User }` to derive them (integer, or UUID when the model has `autoGenerateId = false`) or `{ idType }` to set them. `columnMap.recipientType` adds a `recipient_type` string column. Read-state follows columnMap presence: `readAt` → nullable timestamp, `isRead` → indexed boolean, both → both. Defaults to `user_id` + `read_at` when no columnMap. Spread + extend for extras (FK references, composite indexes). Triggers: `notificationColumns`, `Migration.create` for notifications, "create the notifications table", "notification migration", "add organization_id to notifications", `is_read` column, `read_at` column, `idempotency_key` column; typical import `import { Migration } from "@warlock.js/cascade"; import { notificationColumns } from "@warlock.js/notifications"`. Skip: generic cascade migration writing — the `write-migration` topic of the `warlock-js-cascade` skill; the model itself — the `use-in-app` topic.'
|
|
996
1001
|
---
|
|
997
1002
|
|
|
998
1003
|
# Write the notifications migration
|
|
@@ -1018,6 +1023,7 @@ matching columns:
|
|
|
1018
1023
|
| -------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
1019
1024
|
| _(none / default)_ | `user_id`, `type`, `title`, `body`, `payload`, `read_at`, `idempotency_key` |
|
|
1020
1025
|
| `{ tenant: "organization_id", readAt: "read_at" }` | adds `organization_id`; read_at-only |
|
|
1026
|
+
| `{ recipientType: "recipient_type" }` | adds `recipient_type` (the identity type that owns the row) |
|
|
1021
1027
|
| `{ isRead: "is_read" }` | swaps `read_at` for `is_read` (indexed boolean) |
|
|
1022
1028
|
| `{ isRead: "is_read", readAt: "read_at" }` | both read-state columns |
|
|
1023
1029
|
|
|
@@ -1025,10 +1031,11 @@ The recipient / tenant / read-state column NAMES come from `columnMap`; `type` /
|
|
|
1025
1031
|
`title` / `body` / `payload` / `idempotency_key` are fixed. `id` / `createdAt` /
|
|
1026
1032
|
`updatedAt` are added automatically by cascade.
|
|
1027
1033
|
|
|
1034
|
+
Declare `recipientType` so different identity types sharing an id cannot read each other's notifications (see the `use-in-app` topic). For a table that already exists, add the column with `Migration.alter` and backfill it.
|
|
1035
|
+
|
|
1028
1036
|
## Match your app's identifier type
|
|
1029
1037
|
|
|
1030
|
-
Recipient and tenant columns default to UUID. For
|
|
1031
|
-
are `bigint`, `integer`, or strings, pass `idType`; it is applied to every
|
|
1038
|
+
Recipient and tenant columns default to UUID. For integer-id apps pass `recipientModel: User` (auto-generated ids give `integer`; `autoGenerateId = false` keeps UUID) or set `idType` (`"uuid"`, `"bigint"`, `"integer"`, `"string"`) explicitly; it is applied to every
|
|
1032
1039
|
identifier column the factory creates:
|
|
1033
1040
|
|
|
1034
1041
|
```ts
|
package/llms.txt
CHANGED
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
- [queue-notifications](@warlock.js/notifications/skills/queue-notifications/SKILL.md): Send notifications asynchronously through the herald backend or the BullMQ driver. Configure a QueueDispatcher in config/notifications.ts, then use notif.queue(to, data, options?) to enqueue rendered jobs. Herald uses heraldQueue() and startNotificationsWorker(); bullmqQueue({ attempts, backoff }) from @warlock.js/notifications uses BullMQ (lazy-loading the optional @warlock.js/queue peer), supports retries, and honours SendOptions.delay. Without a queue configured, .queue() rejects NoQueueDispatcherError. Triggers: heraldQueue, bullmqQueue, startNotificationsWorker, QueueDispatcher, .queue(, async notifications, queue notifications, notification worker, background notifications, notifications.dispatch. Skip: synchronous sends - the `define-notification` topic; herald itself - @warlock.js/herald/*.
|
|
16
16
|
- [send-ad-hoc](@warlock.js/notifications/skills/send-ad-hoc/SKILL.md): Ad-hoc per-channel send via the `notify.<channel>(to, payload, options?)` Proxy facade. Works for any channel registered in `NotificationChannels` (built-in or custom). `to` accepts a `Notifiable` model OR a raw route string (`"x@y.com"`); `payload` is typed per channel via the registry; `options` is the standard `SendOptions`. `notify.channel(name).send(...)` is the runtime escape when the channel name is not a compile-time literal. Type-based preference gating applies when a type is known — explicit via `options.type` OR auto-detected from a database payload's `type` field. For multi-channel sends use `defineNotification` instead — there is no inline `notify(to, { via, ... })` form on purpose. Triggers: `notify.<channel>`, `notify.channel(name)`, `notify.mail`, `notify.database`, "send a one-off notification", "ad-hoc notification", "send an email without defining a notification", "raw email target", "dynamic channel name"; typical import `import { notify } from "@warlock.js/notifications"`. Skip: reusable multi-channel notifications — the `define-notification` topic; configuring channels — the `configure-notifications` topic.
|
|
17
17
|
- [use-in-app](@warlock.js/notifications/skills/use-in-app/SKILL.md): Use the `inApp` facade for the in-app/database channel's READ side: `inApp.configure({ model: Notification })` or `{ repository: myRepo }` binds the store AND returns the channel; `inApp.list / listUnread / countUnread / markAsRead / markAsUnread` are RECIPIENT-SCOPED by construction (no IDOR — `markAsRead(user, id)` cannot flip another user's row even with a foreign id). `countUnread` uses `RepositoryManager.countCached` for the badge fast-path. Triggers: `inApp.configure`, `inApp.list`, `inApp.listUnread`, `inApp.countUnread`, `inApp.markAsRead`, `inApp.markAsUnread`, `BaseNotificationsRepository`, `DatabaseNotification`, `NotificationsFilter`, "in-app notifications", "mark notification as read", "unread count", "notification badge", "IDOR-safe markAsRead"; typical import `import { inApp } from "@warlock.js/notifications"`. Skip: writing notifications (use `defineNotification` or `notify.database`) — the `define-notification` topic; the migration columns — the `write-notification-migration` topic; wiring it into config — the `configure-notifications` topic.
|
|
18
|
-
- [write-notification-migration](@warlock.js/notifications/skills/write-notification-migration/SKILL.md): Create the notifications table with the `notificationColumns(Notification)` factory + cascade `Migration.create`. Column NAMES come from the model's `columnMap` (recipient / tenant / readAt / isRead); the fixed columns are `type` / `title` / `body` / `payload` / `idempotency_key`.
|
|
18
|
+
- [write-notification-migration](@warlock.js/notifications/skills/write-notification-migration/SKILL.md): Create the notifications table with the `notificationColumns(Notification)` factory + cascade `Migration.create`. Column NAMES come from the model's `columnMap` (recipient / tenant / readAt / isRead); the fixed columns are `type` / `title` / `body` / `payload` / `idempotency_key`. Id columns default to UUID; pass `{ recipientModel: User }` to derive them (integer, or UUID when the model has `autoGenerateId = false`) or `{ idType }` to set them. `columnMap.recipientType` adds a `recipient_type` string column. Read-state follows columnMap presence: `readAt` → nullable timestamp, `isRead` → indexed boolean, both → both. Defaults to `user_id` + `read_at` when no columnMap. Spread + extend for extras (FK references, composite indexes). Triggers: `notificationColumns`, `Migration.create` for notifications, "create the notifications table", "notification migration", "add organization_id to notifications", `is_read` column, `read_at` column, `idempotency_key` column; typical import `import { Migration } from "@warlock.js/cascade"; import { notificationColumns } from "@warlock.js/notifications"`. Skip: generic cascade migration writing — the `write-migration` topic of the `warlock-js-cascade` skill; the model itself — the `use-in-app` topic.
|
package/package.json
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
"environment": "server"
|
|
6
6
|
},
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@warlock.js/cache": "5.
|
|
9
|
-
"@warlock.js/cascade": "5.
|
|
10
|
-
"@warlock.js/core": "5.
|
|
11
|
-
"@warlock.js/logger": "5.
|
|
8
|
+
"@warlock.js/cache": "5.47.0",
|
|
9
|
+
"@warlock.js/cascade": "5.47.0",
|
|
10
|
+
"@warlock.js/core": "5.47.0",
|
|
11
|
+
"@warlock.js/logger": "5.47.0"
|
|
12
12
|
},
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"@warlock.js/herald": "5.
|
|
15
|
-
"@warlock.js/queue": "5.
|
|
14
|
+
"@warlock.js/herald": "5.47.0",
|
|
15
|
+
"@warlock.js/queue": "5.47.0"
|
|
16
16
|
},
|
|
17
17
|
"peerDependenciesMeta": {
|
|
18
18
|
"@warlock.js/herald": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "hassanzohdy",
|
|
39
39
|
"license": "MIT",
|
|
40
|
-
"version": "5.
|
|
40
|
+
"version": "5.47.0",
|
|
41
41
|
"main": "./cjs/index.cjs",
|
|
42
42
|
"module": "./esm/index.mjs",
|
|
43
43
|
"types": "./esm/index.d.mts",
|
|
@@ -89,13 +89,18 @@ class BaseNotificationsRepository extends RepositoryManager {
|
|
|
89
89
|
|
|
90
90
|
### `createFor` write side — no IDOR either (since 4.15.0)
|
|
91
91
|
|
|
92
|
-
`recipientId`
|
|
92
|
+
`recipientId` (and `recipientType`) are always the trusted arguments, applied **after** the payload — `createFor`/`createManyFor` strip server-owned keys (`id`, `recipientId`, `recipientType`, `tenant`, `readAt`, `isRead`, and their resolved `columnMap` physical column names) off `input` before merging. Before this, a caller passing an untyped payload straight through (request JSON, a channel's `send` args) could smuggle its own `recipientId` — or the raw physical column name — and write the notification into someone else's inbox. `type` / `title` / `body` / `payload` / `idempotencyKey` are untouched; you don't need to sanitize `input` yourself.
|
|
93
|
+
|
|
94
|
+
### Recipient types — identities that share an id
|
|
95
|
+
|
|
96
|
+
Recipients are keyed by `(type, id)`. Declare `recipientType: "recipient_type"` in `columnMap` and every `inApp` read / count / mark / dismiss filters on both, so staff `5` and customer `5` never see each other's rows. The type is the recipient model's `userType` (auth models) or its table name; the `database` channel stores it. Also accepted: `{ type, id }` (`RecipientRef`). `recipientTypeOf(model)` returns the type a model is stored under, and `toRecipientRef(recipient)` turns a model or ref into `{ type, id }`. With the column declared a **bare id throws `RecipientTypeRequiredError`**; without it ids are the only key (single-identity apps, tables created before 5.46.0). Upgrading: add a nullable `recipient_type` column, backfill it (unbackfilled rows match no typed read), then declare it in `columnMap`. Full steps in the docs page "The in-app store".
|
|
93
97
|
|
|
94
98
|
### `columnMap` — map logical roles → your columns
|
|
95
99
|
|
|
96
100
|
```ts
|
|
97
101
|
export type NotificationColumnMap = {
|
|
98
102
|
recipient?: string; // recipient FK. Default "user_id"
|
|
103
|
+
recipientType?: string; // recipient identity type column. Omit → ids are the only key
|
|
99
104
|
tenant?: string; // multi-tenant scope, written from the recipient. Omit → single-tenant
|
|
100
105
|
readAt?: string; // read-timestamp column
|
|
101
106
|
isRead?: string; // read-flag column (indexed)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: write-notification-migration
|
|
3
|
-
description: 'Create the notifications table with the `notificationColumns(Notification)` factory + cascade `Migration.create`. Column NAMES come from the model''s `columnMap` (recipient / tenant / readAt / isRead); the fixed columns are `type` / `title` / `body` / `payload` / `idempotency_key`.
|
|
3
|
+
description: 'Create the notifications table with the `notificationColumns(Notification)` factory + cascade `Migration.create`. Column NAMES come from the model''s `columnMap` (recipient / tenant / readAt / isRead); the fixed columns are `type` / `title` / `body` / `payload` / `idempotency_key`. Id columns default to UUID; pass `{ recipientModel: User }` to derive them (integer, or UUID when the model has `autoGenerateId = false`) or `{ idType }` to set them. `columnMap.recipientType` adds a `recipient_type` string column. Read-state follows columnMap presence: `readAt` → nullable timestamp, `isRead` → indexed boolean, both → both. Defaults to `user_id` + `read_at` when no columnMap. Spread + extend for extras (FK references, composite indexes). Triggers: `notificationColumns`, `Migration.create` for notifications, "create the notifications table", "notification migration", "add organization_id to notifications", `is_read` column, `read_at` column, `idempotency_key` column; typical import `import { Migration } from "@warlock.js/cascade"; import { notificationColumns } from "@warlock.js/notifications"`. Skip: generic cascade migration writing — the `write-migration` topic of the `warlock-js-cascade` skill; the model itself — the `use-in-app` topic.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Write the notifications migration
|
|
@@ -26,6 +26,7 @@ matching columns:
|
|
|
26
26
|
| -------------------------------------------------- | --------------------------------------------------------------------------- |
|
|
27
27
|
| _(none / default)_ | `user_id`, `type`, `title`, `body`, `payload`, `read_at`, `idempotency_key` |
|
|
28
28
|
| `{ tenant: "organization_id", readAt: "read_at" }` | adds `organization_id`; read_at-only |
|
|
29
|
+
| `{ recipientType: "recipient_type" }` | adds `recipient_type` (the identity type that owns the row) |
|
|
29
30
|
| `{ isRead: "is_read" }` | swaps `read_at` for `is_read` (indexed boolean) |
|
|
30
31
|
| `{ isRead: "is_read", readAt: "read_at" }` | both read-state columns |
|
|
31
32
|
|
|
@@ -33,10 +34,11 @@ The recipient / tenant / read-state column NAMES come from `columnMap`; `type` /
|
|
|
33
34
|
`title` / `body` / `payload` / `idempotency_key` are fixed. `id` / `createdAt` /
|
|
34
35
|
`updatedAt` are added automatically by cascade.
|
|
35
36
|
|
|
37
|
+
Declare `recipientType` so different identity types sharing an id cannot read each other's notifications (see the `use-in-app` topic). For a table that already exists, add the column with `Migration.alter` and backfill it.
|
|
38
|
+
|
|
36
39
|
## Match your app's identifier type
|
|
37
40
|
|
|
38
|
-
Recipient and tenant columns default to UUID. For
|
|
39
|
-
are `bigint`, `integer`, or strings, pass `idType`; it is applied to every
|
|
41
|
+
Recipient and tenant columns default to UUID. For integer-id apps pass `recipientModel: User` (auto-generated ids give `integer`; `autoGenerateId = false` keeps UUID) or set `idType` (`"uuid"`, `"bigint"`, `"integer"`, `"string"`) explicitly; it is applied to every
|
|
40
42
|
identifier column the factory creates:
|
|
41
43
|
|
|
42
44
|
```ts
|