@facteurjs/core 2.0.0-beta.1 → 2.0.0-beta.2
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/dist/api/handlers/preferences.mjs +30 -1
- package/dist/channels/fcm/channel.d.mts +2 -1
- package/dist/channels/fcm/channel.mjs +9 -5
- package/dist/database/adapters/knex.mjs +5 -2
- package/dist/database/adapters/kysely.mjs +2 -2
- package/dist/database/types.d.mts +1 -1
- package/dist/facteur.d.mts +6 -0
- package/dist/facteur.mjs +2 -1
- package/dist/fake.d.mts +2 -0
- package/dist/fake.mjs +7 -0
- package/dist/notifications/notification_discoverer.mjs +1 -1
- package/package.json +1 -1
|
@@ -41,12 +41,18 @@ const updatePreferencesRoute = defineRoute(({ facteur, authorize }) => ({
|
|
|
41
41
|
const notifiableId = request.params.notifiableId;
|
|
42
42
|
const tenantId = request.body.tenantId;
|
|
43
43
|
const preferences = request.body.preferences;
|
|
44
|
+
const notificationName = request.body.notificationName;
|
|
45
|
+
const category = request.body.category;
|
|
44
46
|
if (!await checkAuthorization({
|
|
45
47
|
authorize,
|
|
46
48
|
request,
|
|
47
49
|
notifiableId,
|
|
48
50
|
tenantId
|
|
49
51
|
})) return UNAUTHORIZED_RESPONSE;
|
|
52
|
+
if (notificationName && category) return {
|
|
53
|
+
status: 400,
|
|
54
|
+
body: { error: "Cannot specify both \"notificationName\" and \"category\"" }
|
|
55
|
+
};
|
|
50
56
|
if (!preferences) return {
|
|
51
57
|
status: 400,
|
|
52
58
|
body: { error: "Preferences are required" }
|
|
@@ -56,10 +62,33 @@ const updatePreferencesRoute = defineRoute(({ facteur, authorize }) => ({
|
|
|
56
62
|
status: 400,
|
|
57
63
|
body: { error: validation.error }
|
|
58
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Per-category scope: resolve notifications in category and update each one
|
|
67
|
+
*/
|
|
68
|
+
if (category) {
|
|
69
|
+
const matching = (await facteur.discoverer.getNotificationIdentities()).filter((n) => n.category === category);
|
|
70
|
+
if (matching.length === 0) return {
|
|
71
|
+
status: 400,
|
|
72
|
+
body: { error: `No notifications found for category "${category}"` }
|
|
73
|
+
};
|
|
74
|
+
for (const identity of matching) await facteur.db.updatePreferences({
|
|
75
|
+
notifiableId,
|
|
76
|
+
tenantId,
|
|
77
|
+
notificationName: identity.identifier,
|
|
78
|
+
channelPreferences: preferences
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
status: 204,
|
|
82
|
+
body: {}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Global scope (no notificationName) or per-notification scope
|
|
87
|
+
*/
|
|
59
88
|
await facteur.db.updatePreferences({
|
|
60
89
|
notifiableId,
|
|
61
90
|
tenantId,
|
|
62
|
-
notificationName
|
|
91
|
+
notificationName,
|
|
63
92
|
channelPreferences: preferences
|
|
64
93
|
});
|
|
65
94
|
return {
|
|
@@ -2,6 +2,7 @@ import { BatchConfig, BatchSendResult, Channel, ChannelSendParams, kTargetSymbol
|
|
|
2
2
|
import "../../types.mjs";
|
|
3
3
|
import { FcmMessage } from "./message.mjs";
|
|
4
4
|
import { FcmConfig, FcmTargets } from "./types.mjs";
|
|
5
|
+
import { Messaging } from "firebase-admin/messaging";
|
|
5
6
|
import { Awaitable } from "@julr/utils/types";
|
|
6
7
|
|
|
7
8
|
//#region src/channels/fcm/channel.d.ts
|
|
@@ -11,7 +12,7 @@ declare class FcmChannel implements Channel<FcmConfig, FcmMessage, any, FcmTarge
|
|
|
11
12
|
name: "fcm";
|
|
12
13
|
[kTargetSymbol]: FcmTargets;
|
|
13
14
|
batchConfig: BatchConfig;
|
|
14
|
-
constructor(config: FcmConfig);
|
|
15
|
+
constructor(config: FcmConfig, messaging?: Messaging);
|
|
15
16
|
send(options: ChannelSendParams<FcmMessage, FcmTargets>): Promise<string>;
|
|
16
17
|
sendBatch(messages: ChannelSendParams<FcmMessage, FcmTargets>[]): Promise<BatchSendResult>;
|
|
17
18
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { errors } from "../../errors/index.mjs";
|
|
2
2
|
import { kTargetSymbol } from "../../types/channel.mjs";
|
|
3
|
-
import { getMessaging } from "firebase-admin/messaging";
|
|
3
|
+
import { Messaging, getMessaging } from "firebase-admin/messaging";
|
|
4
4
|
import { cert, initializeApp } from "firebase-admin/app";
|
|
5
5
|
|
|
6
6
|
//#region src/channels/fcm/channel.ts
|
|
@@ -16,9 +16,10 @@ var FcmChannel = class {
|
|
|
16
16
|
};
|
|
17
17
|
#messaging;
|
|
18
18
|
#config;
|
|
19
|
-
constructor(config) {
|
|
19
|
+
constructor(config, messaging) {
|
|
20
20
|
this.#config = config;
|
|
21
|
-
this.#messaging =
|
|
21
|
+
if (messaging) this.#messaging = messaging;
|
|
22
|
+
else this.#messaging = getMessaging(initializeApp({
|
|
22
23
|
...config,
|
|
23
24
|
...config.serviceAccountKeyPath ? { credential: cert(config.serviceAccountKeyPath) } : {}
|
|
24
25
|
}));
|
|
@@ -35,8 +36,11 @@ var FcmChannel = class {
|
|
|
35
36
|
#buildMessage(options) {
|
|
36
37
|
const message = options.message.serialize();
|
|
37
38
|
const targets = this.#resolveTargets(options);
|
|
38
|
-
if (this.#config.debugToken)
|
|
39
|
-
|
|
39
|
+
if (this.#config.debugToken) {
|
|
40
|
+
message.token = this.#config.debugToken;
|
|
41
|
+
return message;
|
|
42
|
+
}
|
|
43
|
+
if (targets.token) message.token = targets.token;
|
|
40
44
|
else if (targets.topic) message.topic = targets.topic;
|
|
41
45
|
else if (targets.condition) message.condition = targets.condition;
|
|
42
46
|
return message;
|
|
@@ -97,7 +97,10 @@ var KnexAdapter = class {
|
|
|
97
97
|
}
|
|
98
98
|
async updatePreferences(options) {
|
|
99
99
|
await this.#getConnection().transaction(async (trx) => {
|
|
100
|
-
const existing = await trx.table(this.#preferencesTableName).where("user_id", options.notifiableId).andWhere(
|
|
100
|
+
const existing = await trx.table(this.#preferencesTableName).where("user_id", options.notifiableId).andWhere((builder) => {
|
|
101
|
+
if (options.notificationName) builder.where("notification_name", options.notificationName);
|
|
102
|
+
else builder.whereNull("notification_name");
|
|
103
|
+
}).andWhere((builder) => {
|
|
101
104
|
if (options.tenantId) builder.where("tenant_id", options.tenantId);
|
|
102
105
|
else builder.whereNull("tenant_id");
|
|
103
106
|
}).first();
|
|
@@ -108,7 +111,7 @@ var KnexAdapter = class {
|
|
|
108
111
|
else await trx.table(this.#preferencesTableName).insert({
|
|
109
112
|
user_id: options.notifiableId,
|
|
110
113
|
tenant_id: options.tenantId || null,
|
|
111
|
-
notification_name: options.notificationName,
|
|
114
|
+
notification_name: options.notificationName || null,
|
|
112
115
|
channels: JSON.stringify(options.channelPreferences),
|
|
113
116
|
created_at: /* @__PURE__ */ new Date(),
|
|
114
117
|
updated_at: /* @__PURE__ */ new Date()
|
|
@@ -78,7 +78,7 @@ var KyselyAdapter = class {
|
|
|
78
78
|
}
|
|
79
79
|
async updatePreferences(options) {
|
|
80
80
|
await this.#connection.transaction().execute(async (trx) => {
|
|
81
|
-
const existing = await trx.selectFrom(this.#preferencesTableName).selectAll().where("user_id", "=", options.notifiableId).where("notification_name", "=", options.notificationName).$if(!!options.tenantId, (qb) => qb.where("tenant_id", "=", options.tenantId)).$if(!options.tenantId, (qb) => qb.where("tenant_id", "is", null)).executeTakeFirst();
|
|
81
|
+
const existing = await trx.selectFrom(this.#preferencesTableName).selectAll().where("user_id", "=", options.notifiableId).$if(!!options.notificationName, (qb) => qb.where("notification_name", "=", options.notificationName)).$if(!options.notificationName, (qb) => qb.where("notification_name", "is", null)).$if(!!options.tenantId, (qb) => qb.where("tenant_id", "=", options.tenantId)).$if(!options.tenantId, (qb) => qb.where("tenant_id", "is", null)).executeTakeFirst();
|
|
82
82
|
if (existing) await trx.updateTable(this.#preferencesTableName).set({
|
|
83
83
|
channels: JSON.stringify(options.channelPreferences),
|
|
84
84
|
updated_at: /* @__PURE__ */ new Date()
|
|
@@ -86,7 +86,7 @@ var KyselyAdapter = class {
|
|
|
86
86
|
else await trx.insertInto(this.#preferencesTableName).values({
|
|
87
87
|
user_id: options.notifiableId,
|
|
88
88
|
tenant_id: options.tenantId || null,
|
|
89
|
-
notification_name: options.notificationName,
|
|
89
|
+
notification_name: options.notificationName || null,
|
|
90
90
|
channels: JSON.stringify(options.channelPreferences),
|
|
91
91
|
created_at: /* @__PURE__ */ new Date(),
|
|
92
92
|
updated_at: /* @__PURE__ */ new Date()
|
|
@@ -131,7 +131,7 @@ interface SavePreferencesParams {
|
|
|
131
131
|
interface UpdatePreferencesParams {
|
|
132
132
|
notifiableId: Identifier;
|
|
133
133
|
tenantId?: Identifier;
|
|
134
|
-
notificationName
|
|
134
|
+
notificationName?: string;
|
|
135
135
|
channelPreferences: Record<ChannelName, boolean>;
|
|
136
136
|
}
|
|
137
137
|
/**
|
package/dist/facteur.d.mts
CHANGED
|
@@ -22,6 +22,12 @@ declare class Facteur<KnownChannels extends Record<string, Channel>, DBAdapter e
|
|
|
22
22
|
get discoverer(): {
|
|
23
23
|
discoverNotifications: () => Promise<(new (...args: any[]) => Notification)[]>;
|
|
24
24
|
getNotifications: () => Promise<(new (...args: any[]) => Notification)[]>;
|
|
25
|
+
getNotificationIdentities: () => Promise<{
|
|
26
|
+
name: string;
|
|
27
|
+
identifier: string;
|
|
28
|
+
tags?: string[];
|
|
29
|
+
category?: string;
|
|
30
|
+
}[]>;
|
|
25
31
|
getNotificationTags: () => Promise<string[]>;
|
|
26
32
|
clearCache: () => void;
|
|
27
33
|
};
|
package/dist/facteur.mjs
CHANGED
|
@@ -138,6 +138,7 @@ var Facteur = class {
|
|
|
138
138
|
return {
|
|
139
139
|
discoverNotifications: () => this.#discoverer.discoverNotifications(),
|
|
140
140
|
getNotifications: () => this.#discoverer.getNotifications(),
|
|
141
|
+
getNotificationIdentities: () => this.#discoverer.getNotificationIdentities(),
|
|
141
142
|
getNotificationTags: () => this.#discoverer.getAllNotificationTags(),
|
|
142
143
|
clearCache: () => this.#discoverer.clearCache()
|
|
143
144
|
};
|
|
@@ -146,7 +147,7 @@ var Facteur = class {
|
|
|
146
147
|
* Enable fake mode for testing - captures sent notifications instead of sending
|
|
147
148
|
*/
|
|
148
149
|
fake() {
|
|
149
|
-
this.#fake = new FacteurFake();
|
|
150
|
+
this.#fake = new FacteurFake(() => this.restore());
|
|
150
151
|
return this.#fake;
|
|
151
152
|
}
|
|
152
153
|
/**
|
package/dist/fake.d.mts
CHANGED
package/dist/fake.mjs
CHANGED
|
@@ -3,6 +3,13 @@ import { AssertionError } from "node:assert";
|
|
|
3
3
|
//#region src/fake.ts
|
|
4
4
|
var FacteurFake = class {
|
|
5
5
|
#sentNotifications = [];
|
|
6
|
+
#restoreFn;
|
|
7
|
+
constructor(restoreFn) {
|
|
8
|
+
this.#restoreFn = restoreFn;
|
|
9
|
+
}
|
|
10
|
+
[Symbol.dispose]() {
|
|
11
|
+
this.#restoreFn();
|
|
12
|
+
}
|
|
6
13
|
/**
|
|
7
14
|
* Record a notification as sent during fake mode
|
|
8
15
|
*/
|
|
@@ -90,7 +90,7 @@ var NotificationDiscoverer = class {
|
|
|
90
90
|
const options = NotificationClass.options || {};
|
|
91
91
|
return {
|
|
92
92
|
name: options.name,
|
|
93
|
-
identifier:
|
|
93
|
+
identifier: options.identifier || NotificationClass.name,
|
|
94
94
|
tags: options.tags || [],
|
|
95
95
|
category: options.category
|
|
96
96
|
};
|
package/package.json
CHANGED