@headless-lms/adapter-db 0.3.0 → 0.4.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/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/repositories/admin.js +1 -1
- package/dist/repositories/assets.d.ts +1 -2
- package/dist/repositories/assets.js +0 -4
- package/dist/repositories/assets.js.map +1 -1
- package/dist/repositories/automations.d.ts +0 -1
- package/dist/repositories/automations.js +1 -17
- package/dist/repositories/automations.js.map +1 -1
- package/dist/repositories/chats.d.ts +4 -4
- package/dist/repositories/chats.js +33 -18
- package/dist/repositories/chats.js.map +1 -1
- package/dist/repositories/content.d.ts +3 -3
- package/dist/repositories/content.js +35 -12
- package/dist/repositories/content.js.map +1 -1
- package/dist/repositories/devices.d.ts +17 -0
- package/dist/repositories/devices.js +75 -0
- package/dist/repositories/devices.js.map +1 -0
- package/dist/repositories/digest-runs.d.ts +14 -0
- package/dist/repositories/digest-runs.js +39 -0
- package/dist/repositories/digest-runs.js.map +1 -0
- package/dist/repositories/discussion.d.ts +6 -1
- package/dist/repositories/discussion.js +9 -4
- package/dist/repositories/discussion.js.map +1 -1
- package/dist/repositories/entitlements.d.ts +3 -1
- package/dist/repositories/entitlements.js +12 -6
- package/dist/repositories/entitlements.js.map +1 -1
- package/dist/repositories/inserted-row.d.ts +5 -0
- package/dist/repositories/inserted-row.js +7 -0
- package/dist/repositories/inserted-row.js.map +1 -0
- package/dist/repositories/mentions.d.ts +17 -0
- package/dist/repositories/mentions.js +55 -0
- package/dist/repositories/mentions.js.map +1 -0
- package/dist/repositories/notification-settings.d.ts +19 -0
- package/dist/repositories/notification-settings.js +67 -0
- package/dist/repositories/notification-settings.js.map +1 -0
- package/dist/repositories/notifications.d.ts +23 -16
- package/dist/repositories/notifications.js +152 -44
- package/dist/repositories/notifications.js.map +1 -1
- package/dist/repositories/organizations.d.ts +1 -0
- package/dist/repositories/organizations.js +4 -1
- package/dist/repositories/organizations.js.map +1 -1
- package/dist/repositories/posts.d.ts +4 -8
- package/dist/repositories/posts.js +25 -28
- package/dist/repositories/posts.js.map +1 -1
- package/dist/repositories/subscriptions.d.ts +18 -0
- package/dist/repositories/subscriptions.js +72 -0
- package/dist/repositories/subscriptions.js.map +1 -0
- package/dist/schema/automations.d.ts +524 -19
- package/dist/schema/automations.js +73 -7
- package/dist/schema/automations.js.map +1 -1
- package/dist/schema/discussion.d.ts +277 -127
- package/dist/schema/discussion.js +59 -24
- package/dist/schema/discussion.js.map +1 -1
- package/dist/schema/identity.d.ts +109 -109
- package/dist/schema/identity.js +28 -70
- package/dist/schema/identity.js.map +1 -1
- package/dist/schema/index.d.ts +5 -5
- package/dist/schema/index.js +10 -6
- package/drizzle/0011_digest_runs.sql +8 -0
- package/drizzle/0012_subscriptions.sql +16 -0
- package/drizzle/0013_mentions.sql +10 -0
- package/drizzle/0014_notification_settings_devices.sql +36 -0
- package/drizzle/0015_automations_drop_source.sql +2 -0
- package/drizzle/0016_notifications_automations.sql +48 -0
- package/drizzle/meta/0011_snapshot.json +8868 -0
- package/drizzle/meta/0012_snapshot.json +8873 -0
- package/drizzle/meta/0013_snapshot.json +8943 -0
- package/drizzle/meta/0014_snapshot.json +9305 -0
- package/drizzle/meta/0015_snapshot.json +9270 -0
- package/drizzle/meta/0016_snapshot.json +9230 -0
- package/drizzle/meta/_journal.json +42 -0
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-settings.js","names":[],"sources":["../../src/repositories/notification-settings.ts"],"sourcesContent":["import { and, eq, inArray, sql, type SQL } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { NotificationSettingsRepository, SettingKey } from \"@headless-lms/core/identity\";\nimport type { Logger, StoredSetting } from \"@headless-lms/core/types\";\nimport { genId } from \"@headless-lms/core/shared/id\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { notificationSettings, orgUsers } from \"../schema/index.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nconst ns = notificationSettings;\nconst communityKey = sql`coalesce(${ns.communityId}, '')`;\nconst spaceKey = sql`coalesce(${ns.spaceId}, '')`;\n\nfunction matchesKey(key: SettingKey): SQL {\n return sql`(${communityKey} = ${key.communityId ?? \"\"} and ${spaceKey} = ${key.spaceId ?? \"\"} and ${ns.category} = ${key.category} and ${ns.channel} = ${key.channel})`;\n}\n\nexport class DrizzleNotificationSettingsRepository implements NotificationSettingsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async forUsers(\n orgId: string,\n userIds: string[],\n communityId: string | null,\n categories: string[] | null,\n ): Promise<(StoredSetting & { userId: string })[]> {\n if (userIds.length === 0) {\n return [];\n }\n const conditions = [\n eq(ns.orgId, orgId),\n inArray(ns.userId, userIds),\n sql`${communityKey} = ${communityId ?? \"\"}`,\n ];\n if (categories !== null) {\n if (categories.length === 0) {\n return [];\n }\n conditions.push(inArray(ns.category, categories));\n }\n return this.db\n .select({\n userId: ns.userId,\n communityId: ns.communityId,\n spaceId: ns.spaceId,\n category: ns.category,\n channel: ns.channel,\n enabled: ns.enabled,\n })\n .from(ns)\n .where(and(...conditions));\n }\n\n async apply(\n orgId: string,\n userId: string,\n upserts: (SettingKey & { enabled: boolean })[],\n deletes: SettingKey[],\n at: Date,\n ): Promise<void> {\n if (upserts.length === 0 && deletes.length === 0) {\n return;\n }\n await this.db.transaction(async (tx) => {\n if (deletes.length > 0) {\n await tx\n .delete(ns)\n .where(\n and(\n eq(ns.orgId, orgId),\n eq(ns.userId, userId),\n sql`(${sql.join(deletes.map(matchesKey), sql` or `)})`,\n ),\n );\n }\n if (upserts.length > 0) {\n const rows = upserts.map(\n (row) =>\n sql`(${orgId}, ${genId(\"notificationSetting\")}, ${userId}, ${row.communityId}, ${row.spaceId}, ${row.category}, ${row.channel}, ${row.enabled}, ${at.toISOString()}::timestamptz)`,\n );\n await tx.execute(\n sql`insert into notification_settings (org_id, id, user_id, community_id, space_id, category, channel, enabled, updated_at) values ${sql.join(rows, sql`, `)} on conflict (org_id, user_id, coalesce(community_id, ''), coalesce(space_id, ''), category, channel) do update set enabled = excluded.enabled, updated_at = excluded.updated_at`,\n );\n }\n });\n this.logger.debug(\"notification settings applied\", {\n orgId,\n userId,\n upserts: upserts.length,\n deletes: deletes.length,\n });\n }\n\n async switchedOn(\n orgId: string,\n communityId: string,\n category: string,\n spaceId: string | null,\n ): Promise<string[]> {\n const levels = spaceId === null ? [\"\"] : [spaceId, \"\"];\n const rows = await this.db\n .selectDistinct({ orgUserId: orgUsers.id })\n .from(ns)\n .innerJoin(orgUsers, and(eq(orgUsers.orgId, ns.orgId), eq(orgUsers.userId, ns.userId)))\n .where(\n and(\n eq(ns.orgId, orgId),\n sql`${communityKey} = ${communityId}`,\n eq(ns.category, category),\n sql`${spaceKey} in (${sql.join(\n levels.map((level) => sql`${level}`),\n sql`, `,\n )})`,\n sql`${ns.enabled}`,\n ),\n );\n return rows.map((row) => row.orgUserId);\n }\n}\ntranslateDbErrors(DrizzleNotificationSettingsRepository);\n"],"mappings":"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"}
|
|
@@ -1,22 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { DbExecutor } from "../client.js";
|
|
2
|
+
import { Delivery, NotificationFilter, StoredNotification } from "@headless-lms/core/types";
|
|
3
|
+
import { ListedNotification, NewDelivery, NewNotification, NotificationPage, NotificationsRepository, UnreadCounts } from "@headless-lms/core/automations";
|
|
4
4
|
//#region src/repositories/notifications.d.ts
|
|
5
5
|
declare class DrizzleNotificationsRepository implements NotificationsRepository {
|
|
6
6
|
private readonly db;
|
|
7
|
-
constructor(db:
|
|
8
|
-
insert(orgId: string, rows:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
7
|
+
constructor(db: DbExecutor);
|
|
8
|
+
insert(orgId: string, rows: NewNotification[], deliveries: NewDelivery[]): Promise<void>;
|
|
9
|
+
pendingDeliveriesForEvent(orgId: string, sourceEventId: string): Promise<{
|
|
10
|
+
id: string;
|
|
11
|
+
}[]>;
|
|
12
|
+
findDelivery(orgId: string, deliveryId: string): Promise<{
|
|
13
|
+
delivery: Delivery;
|
|
14
|
+
notification: StoredNotification;
|
|
15
|
+
} | null>;
|
|
16
|
+
markSent(orgId: string, deliveryId: string, at: Date): Promise<void>;
|
|
17
|
+
recordError(orgId: string, deliveryId: string, error: string): Promise<void>;
|
|
18
|
+
markFailed(orgId: string, deliveryId: string, error: string): Promise<void>;
|
|
19
|
+
page(orgId: string, userId: string, filter: NotificationFilter, page: {
|
|
20
|
+
page: number;
|
|
21
|
+
pageSize: number;
|
|
22
|
+
}): Promise<NotificationPage>;
|
|
23
|
+
find(orgId: string, userId: string, notificationId: string): Promise<ListedNotification | null>;
|
|
24
|
+
unreadCounts(orgId: string, userId: string, communityIds: string[]): Promise<UnreadCounts>;
|
|
25
|
+
markRead(orgId: string, userId: string, notificationId: string, at: Date): Promise<boolean>;
|
|
26
|
+
markAllRead(orgId: string, userId: string, filter: NotificationFilter, at: Date): Promise<number>;
|
|
20
27
|
}
|
|
21
28
|
//#endregion
|
|
22
29
|
export { DrizzleNotificationsRepository };
|
|
@@ -1,66 +1,174 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { notificationDeliveries, notifications } from "../schema/automations.js";
|
|
2
2
|
import "../schema/index.js";
|
|
3
3
|
import { translateDbErrors } from "./pg-errors.js";
|
|
4
|
-
import { and, desc, eq, inArray, isNull, sql } from "drizzle-orm";
|
|
4
|
+
import { and, arrayOverlaps, desc, eq, inArray, isNotNull, isNull, or, sql } from "drizzle-orm";
|
|
5
5
|
//#region src/repositories/notifications.ts
|
|
6
|
+
const n = notifications;
|
|
7
|
+
const d = notificationDeliveries;
|
|
8
|
+
function ref(type, id) {
|
|
9
|
+
return type && id ? {
|
|
10
|
+
type,
|
|
11
|
+
id
|
|
12
|
+
} : null;
|
|
13
|
+
}
|
|
14
|
+
function toStored(row) {
|
|
15
|
+
return {
|
|
16
|
+
id: row.id,
|
|
17
|
+
orgId: row.orgId,
|
|
18
|
+
userId: row.userId,
|
|
19
|
+
type: row.type,
|
|
20
|
+
scope: row.scope,
|
|
21
|
+
reasons: row.reasons,
|
|
22
|
+
matchedReason: row.matchedReason,
|
|
23
|
+
category: row.category,
|
|
24
|
+
communityId: row.communityId,
|
|
25
|
+
spaceId: row.spaceId,
|
|
26
|
+
actorOrgUserId: row.actorOrgUserId,
|
|
27
|
+
subject: ref(row.subjectType, row.subjectId),
|
|
28
|
+
parent: ref(row.parentType, row.parentId),
|
|
29
|
+
display: row.display,
|
|
30
|
+
createdAt: row.createdAt
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function toDelivery(row) {
|
|
34
|
+
return {
|
|
35
|
+
id: row.id,
|
|
36
|
+
notificationId: row.notificationId,
|
|
37
|
+
channel: row.channel,
|
|
38
|
+
status: row.status,
|
|
39
|
+
lastError: row.lastError,
|
|
40
|
+
createdAt: row.createdAt,
|
|
41
|
+
sentAt: row.sentAt
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function listed(orgId, userId, filter) {
|
|
45
|
+
const conditions = [
|
|
46
|
+
eq(n.orgId, orgId),
|
|
47
|
+
eq(n.userId, userId),
|
|
48
|
+
eq(n.inApp, true)
|
|
49
|
+
];
|
|
50
|
+
if (filter.scope === "org") conditions.push(isNull(n.communityId));
|
|
51
|
+
if (filter.scope === "community") conditions.push(isNotNull(n.communityId));
|
|
52
|
+
if (filter.communityId !== void 0) {
|
|
53
|
+
const community = eq(n.communityId, filter.communityId);
|
|
54
|
+
conditions.push(filter.includeOrg ? or(community, isNull(n.communityId)) : community);
|
|
55
|
+
}
|
|
56
|
+
if (filter.category && filter.category.length > 0) conditions.push(inArray(n.category, filter.category));
|
|
57
|
+
if (filter.reason && filter.reason.length > 0) conditions.push(arrayOverlaps(n.reasons, filter.reason));
|
|
58
|
+
if (filter.type && filter.type.length > 0) conditions.push(inArray(n.type, filter.type));
|
|
59
|
+
if (filter.unread === true) conditions.push(isNull(n.readAt));
|
|
60
|
+
return conditions;
|
|
61
|
+
}
|
|
6
62
|
var DrizzleNotificationsRepository = class {
|
|
7
63
|
db;
|
|
8
64
|
constructor(db) {
|
|
9
65
|
this.db = db;
|
|
10
66
|
}
|
|
11
|
-
async insert(orgId, rows) {
|
|
67
|
+
async insert(orgId, rows, deliveries) {
|
|
12
68
|
if (rows.length === 0) return;
|
|
13
|
-
await this.db.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
69
|
+
await this.db.transaction(async (tx) => {
|
|
70
|
+
const inserted = await tx.insert(n).values(rows.map((row) => ({
|
|
71
|
+
orgId,
|
|
72
|
+
id: row.id,
|
|
73
|
+
userId: row.userId,
|
|
74
|
+
sourceEventId: row.sourceEventId,
|
|
75
|
+
type: row.type,
|
|
76
|
+
scope: row.scope,
|
|
77
|
+
reasons: row.reasons,
|
|
78
|
+
matchedReason: row.matchedReason,
|
|
79
|
+
category: row.category,
|
|
80
|
+
communityId: row.communityId,
|
|
81
|
+
spaceId: row.spaceId,
|
|
82
|
+
actorOrgUserId: row.actorOrgUserId,
|
|
83
|
+
subjectType: row.subject?.type ?? null,
|
|
84
|
+
subjectId: row.subject?.id ?? null,
|
|
85
|
+
parentType: row.parent?.type ?? null,
|
|
86
|
+
parentId: row.parent?.id ?? null,
|
|
87
|
+
display: row.display,
|
|
88
|
+
inApp: row.inApp,
|
|
89
|
+
createdAt: row.createdAt
|
|
90
|
+
}))).onConflictDoNothing().returning({ id: n.id });
|
|
91
|
+
const kept = new Set(inserted.map((row) => row.id));
|
|
92
|
+
const pending = deliveries.filter((delivery) => kept.has(delivery.notificationId));
|
|
93
|
+
if (pending.length === 0) return;
|
|
94
|
+
await tx.insert(d).values(pending.map((delivery) => ({
|
|
95
|
+
orgId,
|
|
96
|
+
id: delivery.id,
|
|
97
|
+
notificationId: delivery.notificationId,
|
|
98
|
+
channel: delivery.channel,
|
|
99
|
+
status: "pending"
|
|
100
|
+
}))).onConflictDoNothing();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async pendingDeliveriesForEvent(orgId, sourceEventId) {
|
|
104
|
+
return this.db.select({ id: d.id }).from(d).innerJoin(n, and(eq(n.orgId, d.orgId), eq(n.id, d.notificationId))).where(and(eq(n.orgId, orgId), eq(n.sourceEventId, sourceEventId), eq(d.status, "pending"))).orderBy(d.id);
|
|
105
|
+
}
|
|
106
|
+
async findDelivery(orgId, deliveryId) {
|
|
107
|
+
const [row] = await this.db.select({
|
|
108
|
+
delivery: d,
|
|
109
|
+
notification: n
|
|
110
|
+
}).from(d).innerJoin(n, and(eq(n.orgId, d.orgId), eq(n.id, d.notificationId))).where(and(eq(d.orgId, orgId), eq(d.id, deliveryId))).limit(1);
|
|
111
|
+
return row ? {
|
|
112
|
+
delivery: toDelivery(row.delivery),
|
|
113
|
+
notification: toStored(row.notification)
|
|
114
|
+
} : null;
|
|
115
|
+
}
|
|
116
|
+
async markSent(orgId, deliveryId, at) {
|
|
117
|
+
await this.db.update(d).set({
|
|
118
|
+
status: "sent",
|
|
119
|
+
sentAt: at
|
|
120
|
+
}).where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, "pending")));
|
|
121
|
+
}
|
|
122
|
+
async recordError(orgId, deliveryId, error) {
|
|
123
|
+
await this.db.update(d).set({ lastError: error }).where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, "pending")));
|
|
17
124
|
}
|
|
18
|
-
async
|
|
19
|
-
|
|
20
|
-
|
|
125
|
+
async markFailed(orgId, deliveryId, error) {
|
|
126
|
+
await this.db.update(d).set({
|
|
127
|
+
status: "failed",
|
|
128
|
+
lastError: error
|
|
129
|
+
}).where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, "pending")));
|
|
130
|
+
}
|
|
131
|
+
async page(orgId, userId, filter, page) {
|
|
132
|
+
const where = and(...listed(orgId, userId, filter));
|
|
133
|
+
const [rows, counts] = await Promise.all([this.db.select().from(n).where(where).orderBy(desc(n.createdAt), desc(n.id)).limit(page.pageSize).offset((page.page - 1) * page.pageSize), this.db.select({
|
|
21
134
|
total: sql`count(*)::int`,
|
|
22
|
-
unread: sql`(count(*) filter (where ${
|
|
23
|
-
}).from(
|
|
135
|
+
unread: sql`(count(*) filter (where ${n.readAt} is null))::int`
|
|
136
|
+
}).from(n).where(where)]);
|
|
24
137
|
return {
|
|
25
|
-
rows
|
|
138
|
+
items: rows.map((row) => ({
|
|
139
|
+
...toStored(row),
|
|
140
|
+
readAt: row.readAt
|
|
141
|
+
})),
|
|
26
142
|
total: Number(counts[0]?.total ?? 0),
|
|
27
143
|
unreadCount: Number(counts[0]?.unread ?? 0)
|
|
28
144
|
};
|
|
29
145
|
}
|
|
30
|
-
async
|
|
31
|
-
const [row] = await this.db.
|
|
32
|
-
return row
|
|
146
|
+
async find(orgId, userId, notificationId) {
|
|
147
|
+
const [row] = await this.db.select().from(n).where(and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.id, notificationId), eq(n.inApp, true))).limit(1);
|
|
148
|
+
return row ? {
|
|
149
|
+
...toStored(row),
|
|
150
|
+
readAt: row.readAt
|
|
151
|
+
} : null;
|
|
33
152
|
}
|
|
34
|
-
async
|
|
35
|
-
|
|
153
|
+
async unreadCounts(orgId, userId, communityIds) {
|
|
154
|
+
const unread = and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.inApp, true), isNull(n.readAt));
|
|
155
|
+
const [org, communities] = await Promise.all([this.db.select({ unread: sql`count(*)::int` }).from(n).where(and(unread, isNull(n.communityId))), communityIds.length === 0 ? Promise.resolve([]) : this.db.select({
|
|
156
|
+
communityId: n.communityId,
|
|
157
|
+
unread: sql`count(*)::int`
|
|
158
|
+
}).from(n).where(and(unread, inArray(n.communityId, communityIds))).groupBy(n.communityId)]);
|
|
159
|
+
return {
|
|
160
|
+
org: Number(org[0]?.unread ?? 0),
|
|
161
|
+
communities: communities.flatMap((row) => row.communityId ? [{
|
|
162
|
+
communityId: row.communityId,
|
|
163
|
+
unread: Number(row.unread)
|
|
164
|
+
}] : [])
|
|
165
|
+
};
|
|
36
166
|
}
|
|
37
|
-
async
|
|
38
|
-
|
|
39
|
-
return this.db.select({
|
|
40
|
-
userId: notificationPreferences.userId,
|
|
41
|
-
kind: notificationPreferences.kind,
|
|
42
|
-
inApp: notificationPreferences.inApp,
|
|
43
|
-
email: notificationPreferences.email
|
|
44
|
-
}).from(notificationPreferences).where(and(eq(notificationPreferences.orgId, orgId), inArray(notificationPreferences.userId, userIds)));
|
|
167
|
+
async markRead(orgId, userId, notificationId, at) {
|
|
168
|
+
return (await this.db.update(n).set({ readAt: sql`coalesce(${n.readAt}, ${at.toISOString()}::timestamptz)` }).where(and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.id, notificationId), eq(n.inApp, true))).returning({ id: n.id })).length > 0;
|
|
45
169
|
}
|
|
46
|
-
async
|
|
47
|
-
|
|
48
|
-
await this.db.insert(notificationPreferences).values(preferences.map((pref) => ({
|
|
49
|
-
orgId,
|
|
50
|
-
userId,
|
|
51
|
-
...pref
|
|
52
|
-
}))).onConflictDoUpdate({
|
|
53
|
-
target: [
|
|
54
|
-
notificationPreferences.orgId,
|
|
55
|
-
notificationPreferences.userId,
|
|
56
|
-
notificationPreferences.kind
|
|
57
|
-
],
|
|
58
|
-
set: {
|
|
59
|
-
inApp: sql`excluded.in_app`,
|
|
60
|
-
email: sql`excluded.email`,
|
|
61
|
-
updatedAt: sql`now()`
|
|
62
|
-
}
|
|
63
|
-
});
|
|
170
|
+
async markAllRead(orgId, userId, filter, at) {
|
|
171
|
+
return (await this.db.update(n).set({ readAt: at }).where(and(...listed(orgId, userId, filter), isNull(n.readAt))).returning({ id: n.id })).length;
|
|
64
172
|
}
|
|
65
173
|
};
|
|
66
174
|
translateDbErrors(DrizzleNotificationsRepository);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notifications.js","names":[],"sources":["../../src/repositories/notifications.ts"],"sourcesContent":["import { and, desc, eq, inArray, isNull, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport type { NotificationsQuery, NotificationsRepository } from \"@headless-lms/core/identity\";\nimport type { Notification, NotificationPreference } from \"@headless-lms/core/types\";\nimport { notificationPreferences, notifications } from \"../schema/index.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nexport class DrizzleNotificationsRepository implements NotificationsRepository {\n constructor(private readonly db: NodePgDatabase) {}\n\n async insert(orgId: string, rows: Notification[]): Promise<void> {\n if (rows.length === 0) {\n return;\n }\n await this.db.insert(notifications).values(rows.map((row) => ({ ...row, orgId })));\n }\n\n async page(\n orgId: string,\n userId: string,\n query: NotificationsQuery,\n ): Promise<{ rows: Notification[]; total: number; unreadCount: number }> {\n const mine = and(eq(notifications.orgId, orgId), eq(notifications.userId, userId));\n const [rows, counts] = await Promise.all([\n this.db\n .select()\n .from(notifications)\n .where(mine)\n .orderBy(desc(notifications.createdAt), desc(notifications.id))\n .limit(query.pageSize)\n .offset((query.page - 1) * query.pageSize),\n this.db\n .select({\n total: sql<number>`count(*)::int`,\n unread: sql<number>`(count(*) filter (where ${notifications.readAt} is null))::int`,\n })\n .from(notifications)\n .where(mine),\n ]);\n return {\n rows,\n total: Number(counts[0]?.total ?? 0),\n unreadCount: Number(counts[0]?.unread ?? 0),\n };\n }\n\n async markRead(\n orgId: string,\n userId: string,\n notificationId: string,\n at: Date,\n ): Promise<Notification | null> {\n const [row] = await this.db\n .update(notifications)\n .set({ readAt: sql`coalesce(${notifications.readAt}, ${at.toISOString()}::timestamptz)` })\n .where(\n and(\n eq(notifications.orgId, orgId),\n eq(notifications.userId, userId),\n eq(notifications.id, notificationId),\n ),\n )\n .returning();\n return row ?? null;\n }\n\n async markAllRead(orgId: string, userId: string, at: Date): Promise<number> {\n const rows = await this.db\n .update(notifications)\n .set({ readAt: at })\n .where(\n and(\n eq(notifications.orgId, orgId),\n eq(notifications.userId, userId),\n isNull(notifications.readAt),\n ),\n )\n .returning({ id: notifications.id });\n return rows.length;\n }\n\n async preferences(\n orgId: string,\n userIds: string[],\n ): Promise<(NotificationPreference & { userId: string })[]> {\n if (userIds.length === 0) {\n return [];\n }\n return this.db\n .select({\n userId: notificationPreferences.userId,\n kind: notificationPreferences.kind,\n inApp: notificationPreferences.inApp,\n email: notificationPreferences.email,\n })\n .from(notificationPreferences)\n .where(\n and(\n eq(notificationPreferences.orgId, orgId),\n inArray(notificationPreferences.userId, userIds),\n ),\n );\n }\n\n async upsertPreferences(\n orgId: string,\n userId: string,\n preferences: NotificationPreference[],\n ): Promise<void> {\n if (preferences.length === 0) {\n return;\n }\n await this.db\n .insert(notificationPreferences)\n .values(preferences.map((pref) => ({ orgId, userId, ...pref })))\n .onConflictDoUpdate({\n target: [\n notificationPreferences.orgId,\n notificationPreferences.userId,\n notificationPreferences.kind,\n ],\n set: {\n inApp: sql`excluded.in_app`,\n email: sql`excluded.email`,\n updatedAt: sql`now()`,\n },\n });\n }\n}\n\ntranslateDbErrors(DrizzleNotificationsRepository);\n"],"mappings":";;;;;AAOA,IAAa,iCAAb,MAA+E;CAChD;CAA7B,YAAY,IAAqC;EAApB,KAAA,KAAA;CAAqB;CAElD,MAAM,OAAO,OAAe,MAAqC;EAC/D,IAAI,KAAK,WAAW,GAClB;EAEF,MAAM,KAAK,GAAG,OAAO,aAAa,CAAC,CAAC,OAAO,KAAK,KAAK,SAAS;GAAE,GAAG;GAAK;EAAM,EAAE,CAAC;CACnF;CAEA,MAAM,KACJ,OACA,QACA,OACuE;EACvE,MAAM,OAAO,IAAI,GAAG,cAAc,OAAO,KAAK,GAAG,GAAG,cAAc,QAAQ,MAAM,CAAC;EACjF,MAAM,CAAC,MAAM,UAAU,MAAM,QAAQ,IAAI,CACvC,KAAK,GACF,OAAO,CAAC,CACR,KAAK,aAAa,CAAC,CACnB,MAAM,IAAI,CAAC,CACX,QAAQ,KAAK,cAAc,SAAS,GAAG,KAAK,cAAc,EAAE,CAAC,CAAC,CAC9D,MAAM,MAAM,QAAQ,CAAC,CACrB,QAAQ,MAAM,OAAO,KAAK,MAAM,QAAQ,GAC3C,KAAK,GACF,OAAO;GACN,OAAO,GAAW;GAClB,QAAQ,GAAW,2BAA2B,cAAc,OAAO;EACrE,CAAC,CAAC,CACD,KAAK,aAAa,CAAC,CACnB,MAAM,IAAI,CACf,CAAC;EACD,OAAO;GACL;GACA,OAAO,OAAO,OAAO,EAAE,EAAE,SAAS,CAAC;GACnC,aAAa,OAAO,OAAO,EAAE,EAAE,UAAU,CAAC;EAC5C;CACF;CAEA,MAAM,SACJ,OACA,QACA,gBACA,IAC8B;EAC9B,MAAM,CAAC,OAAO,MAAM,KAAK,GACtB,OAAO,aAAa,CAAC,CACrB,IAAI,EAAE,QAAQ,GAAG,YAAY,cAAc,OAAO,IAAI,GAAG,YAAY,EAAE,gBAAgB,CAAC,CAAC,CACzF,MACC,IACE,GAAG,cAAc,OAAO,KAAK,GAC7B,GAAG,cAAc,QAAQ,MAAM,GAC/B,GAAG,cAAc,IAAI,cAAc,CACrC,CACF,CAAC,CACA,UAAU;EACb,OAAO,OAAO;CAChB;CAEA,MAAM,YAAY,OAAe,QAAgB,IAA2B;EAY1E,QAAO,MAXY,KAAK,GACrB,OAAO,aAAa,CAAC,CACrB,IAAI,EAAE,QAAQ,GAAG,CAAC,CAAC,CACnB,MACC,IACE,GAAG,cAAc,OAAO,KAAK,GAC7B,GAAG,cAAc,QAAQ,MAAM,GAC/B,OAAO,cAAc,MAAM,CAC7B,CACF,CAAC,CACA,UAAU,EAAE,IAAI,cAAc,GAAG,CAAC,EAAA,CACzB;CACd;CAEA,MAAM,YACJ,OACA,SAC0D;EAC1D,IAAI,QAAQ,WAAW,GACrB,OAAO,CAAC;EAEV,OAAO,KAAK,GACT,OAAO;GACN,QAAQ,wBAAwB;GAChC,MAAM,wBAAwB;GAC9B,OAAO,wBAAwB;GAC/B,OAAO,wBAAwB;EACjC,CAAC,CAAC,CACD,KAAK,uBAAuB,CAAC,CAC7B,MACC,IACE,GAAG,wBAAwB,OAAO,KAAK,GACvC,QAAQ,wBAAwB,QAAQ,OAAO,CACjD,CACF;CACJ;CAEA,MAAM,kBACJ,OACA,QACA,aACe;EACf,IAAI,YAAY,WAAW,GACzB;EAEF,MAAM,KAAK,GACR,OAAO,uBAAuB,CAAC,CAC/B,OAAO,YAAY,KAAK,UAAU;GAAE;GAAO;GAAQ,GAAG;EAAK,EAAE,CAAC,CAAC,CAC/D,mBAAmB;GAClB,QAAQ;IACN,wBAAwB;IACxB,wBAAwB;IACxB,wBAAwB;GAC1B;GACA,KAAK;IACH,OAAO,GAAG;IACV,OAAO,GAAG;IACV,WAAW,GAAG;GAChB;EACF,CAAC;CACL;AACF;AAEA,kBAAkB,8BAA8B"}
|
|
1
|
+
{"version":3,"file":"notifications.js","names":[],"sources":["../../src/repositories/notifications.ts"],"sourcesContent":["import {\n and,\n arrayOverlaps,\n desc,\n eq,\n inArray,\n isNotNull,\n isNull,\n or,\n sql,\n type SQL,\n} from \"drizzle-orm\";\nimport type {\n ListedNotification,\n NewDelivery,\n NewNotification,\n NotificationPage,\n NotificationsRepository,\n UnreadCounts,\n} from \"@headless-lms/core/automations\";\nimport type {\n Delivery,\n NotificationFilter,\n StoredNotification,\n SubjectRef,\n} from \"@headless-lms/core/types\";\nimport type { DbExecutor } from \"../client.js\";\nimport { notificationDeliveries, notifications } from \"../schema/index.js\";\nimport { translateDbErrors } from \"./pg-errors.js\";\n\nconst n = notifications;\nconst d = notificationDeliveries;\n\ntype NotificationRow = typeof notifications.$inferSelect;\ntype DeliveryRow = typeof notificationDeliveries.$inferSelect;\n\nfunction ref(type: string | null, id: string | null): SubjectRef | null {\n return type && id ? { type: type as SubjectRef[\"type\"], id } : null;\n}\n\nfunction toStored(row: NotificationRow): StoredNotification {\n return {\n id: row.id,\n orgId: row.orgId,\n userId: row.userId,\n type: row.type,\n scope: row.scope,\n reasons: row.reasons,\n matchedReason: row.matchedReason,\n category: row.category,\n communityId: row.communityId,\n spaceId: row.spaceId,\n actorOrgUserId: row.actorOrgUserId,\n subject: ref(row.subjectType, row.subjectId),\n parent: ref(row.parentType, row.parentId),\n display: row.display,\n createdAt: row.createdAt,\n };\n}\n\nfunction toDelivery(row: DeliveryRow): Delivery {\n return {\n id: row.id,\n notificationId: row.notificationId,\n channel: row.channel,\n status: row.status,\n lastError: row.lastError,\n createdAt: row.createdAt,\n sentAt: row.sentAt,\n };\n}\n\nfunction listed(orgId: string, userId: string, filter: NotificationFilter): SQL[] {\n const conditions: SQL[] = [eq(n.orgId, orgId), eq(n.userId, userId), eq(n.inApp, true)];\n if (filter.scope === \"org\") {\n conditions.push(isNull(n.communityId));\n }\n if (filter.scope === \"community\") {\n conditions.push(isNotNull(n.communityId));\n }\n if (filter.communityId !== undefined) {\n const community = eq(n.communityId, filter.communityId);\n conditions.push(filter.includeOrg ? or(community, isNull(n.communityId))! : community);\n }\n if (filter.category && filter.category.length > 0) {\n conditions.push(inArray(n.category, filter.category));\n }\n if (filter.reason && filter.reason.length > 0) {\n conditions.push(arrayOverlaps(n.reasons, filter.reason));\n }\n if (filter.type && filter.type.length > 0) {\n conditions.push(inArray(n.type, filter.type));\n }\n if (filter.unread === true) {\n conditions.push(isNull(n.readAt));\n }\n return conditions;\n}\n\nexport class DrizzleNotificationsRepository implements NotificationsRepository {\n constructor(private readonly db: DbExecutor) {}\n\n async insert(orgId: string, rows: NewNotification[], deliveries: NewDelivery[]): Promise<void> {\n if (rows.length === 0) {\n return;\n }\n await this.db.transaction(async (tx) => {\n const inserted = await tx\n .insert(n)\n .values(\n rows.map((row) => ({\n orgId,\n id: row.id,\n userId: row.userId,\n sourceEventId: row.sourceEventId,\n type: row.type,\n scope: row.scope,\n reasons: row.reasons,\n matchedReason: row.matchedReason,\n category: row.category,\n communityId: row.communityId,\n spaceId: row.spaceId,\n actorOrgUserId: row.actorOrgUserId,\n subjectType: row.subject?.type ?? null,\n subjectId: row.subject?.id ?? null,\n parentType: row.parent?.type ?? null,\n parentId: row.parent?.id ?? null,\n display: row.display,\n inApp: row.inApp,\n createdAt: row.createdAt,\n })),\n )\n .onConflictDoNothing()\n .returning({ id: n.id });\n const kept = new Set(inserted.map((row) => row.id));\n const pending = deliveries.filter((delivery) => kept.has(delivery.notificationId));\n if (pending.length === 0) {\n return;\n }\n await tx\n .insert(d)\n .values(\n pending.map((delivery) => ({\n orgId,\n id: delivery.id,\n notificationId: delivery.notificationId,\n channel: delivery.channel,\n status: \"pending\" as const,\n })),\n )\n .onConflictDoNothing();\n });\n }\n\n async pendingDeliveriesForEvent(orgId: string, sourceEventId: string): Promise<{ id: string }[]> {\n return this.db\n .select({ id: d.id })\n .from(d)\n .innerJoin(n, and(eq(n.orgId, d.orgId), eq(n.id, d.notificationId)))\n .where(and(eq(n.orgId, orgId), eq(n.sourceEventId, sourceEventId), eq(d.status, \"pending\")))\n .orderBy(d.id);\n }\n\n async findDelivery(\n orgId: string,\n deliveryId: string,\n ): Promise<{ delivery: Delivery; notification: StoredNotification } | null> {\n const [row] = await this.db\n .select({ delivery: d, notification: n })\n .from(d)\n .innerJoin(n, and(eq(n.orgId, d.orgId), eq(n.id, d.notificationId)))\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId)))\n .limit(1);\n return row\n ? { delivery: toDelivery(row.delivery), notification: toStored(row.notification) }\n : null;\n }\n\n async markSent(orgId: string, deliveryId: string, at: Date): Promise<void> {\n await this.db\n .update(d)\n .set({ status: \"sent\", sentAt: at })\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, \"pending\")));\n }\n\n async recordError(orgId: string, deliveryId: string, error: string): Promise<void> {\n await this.db\n .update(d)\n .set({ lastError: error })\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, \"pending\")));\n }\n\n async markFailed(orgId: string, deliveryId: string, error: string): Promise<void> {\n await this.db\n .update(d)\n .set({ status: \"failed\", lastError: error })\n .where(and(eq(d.orgId, orgId), eq(d.id, deliveryId), eq(d.status, \"pending\")));\n }\n\n async page(\n orgId: string,\n userId: string,\n filter: NotificationFilter,\n page: { page: number; pageSize: number },\n ): Promise<NotificationPage> {\n const where = and(...listed(orgId, userId, filter));\n const [rows, counts] = await Promise.all([\n this.db\n .select()\n .from(n)\n .where(where)\n .orderBy(desc(n.createdAt), desc(n.id))\n .limit(page.pageSize)\n .offset((page.page - 1) * page.pageSize),\n this.db\n .select({\n total: sql<number>`count(*)::int`,\n unread: sql<number>`(count(*) filter (where ${n.readAt} is null))::int`,\n })\n .from(n)\n .where(where),\n ]);\n return {\n items: rows.map((row) => ({ ...toStored(row), readAt: row.readAt })),\n total: Number(counts[0]?.total ?? 0),\n unreadCount: Number(counts[0]?.unread ?? 0),\n };\n }\n\n async find(\n orgId: string,\n userId: string,\n notificationId: string,\n ): Promise<ListedNotification | null> {\n const [row] = await this.db\n .select()\n .from(n)\n .where(\n and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.id, notificationId), eq(n.inApp, true)),\n )\n .limit(1);\n return row ? { ...toStored(row), readAt: row.readAt } : null;\n }\n\n async unreadCounts(orgId: string, userId: string, communityIds: string[]): Promise<UnreadCounts> {\n const unread = and(\n eq(n.orgId, orgId),\n eq(n.userId, userId),\n eq(n.inApp, true),\n isNull(n.readAt),\n );\n const [org, communities] = await Promise.all([\n this.db\n .select({ unread: sql<number>`count(*)::int` })\n .from(n)\n .where(and(unread, isNull(n.communityId))),\n communityIds.length === 0\n ? Promise.resolve([])\n : this.db\n .select({ communityId: n.communityId, unread: sql<number>`count(*)::int` })\n .from(n)\n .where(and(unread, inArray(n.communityId, communityIds)))\n .groupBy(n.communityId),\n ]);\n return {\n org: Number(org[0]?.unread ?? 0),\n communities: communities.flatMap((row) =>\n row.communityId ? [{ communityId: row.communityId, unread: Number(row.unread) }] : [],\n ),\n };\n }\n\n async markRead(\n orgId: string,\n userId: string,\n notificationId: string,\n at: Date,\n ): Promise<boolean> {\n const rows = await this.db\n .update(n)\n .set({ readAt: sql`coalesce(${n.readAt}, ${at.toISOString()}::timestamptz)` })\n .where(\n and(eq(n.orgId, orgId), eq(n.userId, userId), eq(n.id, notificationId), eq(n.inApp, true)),\n )\n .returning({ id: n.id });\n return rows.length > 0;\n }\n\n async markAllRead(\n orgId: string,\n userId: string,\n filter: NotificationFilter,\n at: Date,\n ): Promise<number> {\n const rows = await this.db\n .update(n)\n .set({ readAt: at })\n .where(and(...listed(orgId, userId, filter), isNull(n.readAt)))\n .returning({ id: n.id });\n return rows.length;\n }\n}\n\ntranslateDbErrors(DrizzleNotificationsRepository);\n"],"mappings":"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"}
|
|
@@ -10,6 +10,7 @@ declare class DrizzleOrganizationsRepository implements OrganizationsRepository
|
|
|
10
10
|
update(id: string, input: UpdateOrganizationInput): Promise<Organization | null>;
|
|
11
11
|
delete(id: string): Promise<Organization>;
|
|
12
12
|
findById(id: string): Promise<Organization | null>;
|
|
13
|
+
listIds(): Promise<string[]>;
|
|
13
14
|
findByExternalId(externalId: string): Promise<Organization | null>;
|
|
14
15
|
findBySlug(slug: string): Promise<Organization | null>;
|
|
15
16
|
upsertPendingInvite(orgId: string, input: NewInviteRow): Promise<Invite>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { invites, orgUsers, organizations } from "../schema/organizations.js";
|
|
2
2
|
import { isUniqueViolation, pgErrorFields, translateDbErrors } from "./pg-errors.js";
|
|
3
|
-
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
3
|
+
import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
|
4
4
|
import { ConflictError } from "@headless-lms/core/shared/errors";
|
|
5
5
|
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
6
6
|
import { parseRole } from "@headless-lms/core/organizations";
|
|
@@ -60,6 +60,9 @@ var DrizzleOrganizationsRepository = class {
|
|
|
60
60
|
const [row] = await this.db.select().from(organizations).where(eq(organizations.id, id));
|
|
61
61
|
return row ?? null;
|
|
62
62
|
}
|
|
63
|
+
async listIds() {
|
|
64
|
+
return (await this.db.select({ id: organizations.id }).from(organizations).orderBy(asc(organizations.id))).map((row) => row.id);
|
|
65
|
+
}
|
|
63
66
|
async findByExternalId(externalId) {
|
|
64
67
|
const [row] = await this.db.select().from(organizations).where(eq(organizations.externalId, externalId)).limit(1);
|
|
65
68
|
return row ?? null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"organizations.js","names":[],"sources":["../../src/repositories/organizations.ts"],"sourcesContent":["// organizations — Drizzle repository (implements the core outbound port).\nimport { and, eq, inArray, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport {\n parseRole,\n type CreateOrgUserInput,\n type Invite,\n type NewInviteRow,\n type NewOrganizationRow,\n type Organization,\n type OrganizationsRepository,\n type OrgUser,\n type Role,\n type UpdateOrganizationInput,\n type UpdateOrgUserInput,\n} from \"@headless-lms/core/organizations\";\nimport { invites, organizations, orgUsers } from \"../schema/organizations.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { ConflictError } from \"@headless-lms/core/shared/errors\";\nimport { isUniqueViolation, pgErrorFields, translateDbErrors } from \"./pg-errors.js\";\n\nconst INVITE_STATUSES = [\"pending\", \"accepted\", \"rejected\", \"canceled\"] as const;\ntype InviteStatus = (typeof INVITE_STATUSES)[number];\nconst toStatus = (s: string): InviteStatus =>\n (INVITE_STATUSES as readonly string[]).includes(s) ? (s as InviteStatus) : \"pending\";\n\nfunction toInvite(row: typeof invites.$inferSelect): Invite {\n return {\n id: row.id,\n orgId: row.orgId,\n email: row.email,\n role: row.role,\n status: row.status,\n invitedBy: row.invitedBy,\n tokenHash: row.tokenHash,\n expiresAt: row.expiresAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleOrganizationsRepository implements OrganizationsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async create(input: NewOrganizationRow): Promise<Organization> {\n const [row] = await this.db\n .insert(organizations)\n .values({\n externalId: input.externalId,\n name: input.name,\n slug: input.slug,\n ownerId: input.ownerId,\n id: input.id,\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert organization\");\n }\n return row!;\n }\n\n async update(id: string, input: UpdateOrganizationInput): Promise<Organization | null> {\n const [row] = await this.db\n .update(organizations)\n .set({ name: input.name, slug: input.slug })\n .where(eq(organizations.id, id))\n .returning();\n return row ?? null;\n }\n\n async delete(id: string): Promise<Organization> {\n const [row] = await this.db.delete(organizations).where(eq(organizations.id, id)).returning();\n\n if (!row) {\n throw new Error(\"failed to delete organization\");\n }\n return row;\n }\n\n async findById(id: string): Promise<Organization | null> {\n const [row] = await this.db.select().from(organizations).where(eq(organizations.id, id));\n return row ?? null;\n }\n\n async findByExternalId(externalId: string): Promise<Organization | null> {\n const [row] = await this.db\n .select()\n .from(organizations)\n .where(eq(organizations.externalId, externalId))\n .limit(1);\n return row ?? null;\n }\n\n async findBySlug(slug: string): Promise<Organization | null> {\n const [row] = await this.db\n .select()\n .from(organizations)\n .where(eq(organizations.slug, slug))\n .limit(1);\n return row ?? null;\n }\n\n async upsertPendingInvite(orgId: string, input: NewInviteRow): Promise<Invite> {\n const [row] = await this.db\n .insert(invites)\n .values({\n orgId,\n email: input.email,\n role: input.role,\n status: \"pending\",\n invitedBy: input.invitedBy,\n tokenHash: input.tokenHash,\n expiresAt: input.expiresAt,\n })\n .onConflictDoUpdate({\n target: [invites.orgId, invites.email],\n targetWhere: sql`${invites.status} = 'pending'`,\n set: {\n role: input.role,\n invitedBy: input.invitedBy,\n tokenHash: input.tokenHash,\n expiresAt: input.expiresAt,\n },\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to upsert invite\");\n }\n return toInvite(row);\n }\n\n async setInviteStatus(orgId: string, id: string, status: string): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ status: toStatus(status) })\n .where(and(eq(invites.orgId, orgId), eq(invites.id, id)))\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async findInviteByTokenHash(tokenHash: string): Promise<Invite | null> {\n const [row] = await this.db\n .select()\n .from(invites)\n .where(eq(invites.tokenHash, tokenHash))\n .limit(1);\n return row ? toInvite(row) : null;\n }\n\n async findPendingInvite(orgId: string, email: string): Promise<Invite | null> {\n const [row] = await this.db\n .select()\n .from(invites)\n .where(\n and(\n eq(invites.orgId, orgId),\n sql`lower(${invites.email}) = lower(${email})`,\n eq(invites.status, \"pending\"),\n ),\n )\n .limit(1);\n return row ? toInvite(row) : null;\n }\n\n async setInviteToken(\n orgId: string,\n id: string,\n tokenHash: string,\n expiresAt: Date,\n ): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ tokenHash, expiresAt })\n .where(and(eq(invites.orgId, orgId), eq(invites.id, id), eq(invites.status, \"pending\")))\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async findOrgUser(orgId: string, userId: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.userId, userId)))\n .limit(1);\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findOrgUsersByUser(userId: string): Promise<OrgUser[]> {\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(eq(orgUsers.userId, userId))\n .orderBy(orgUsers.createdAt);\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async findOrgUserById(orgId: string, id: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .limit(1);\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findOrgUsersByIds(orgId: string, ids: string[]): Promise<OrgUser[]> {\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, ids)));\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async findOrgUserIdsByRole(orgId: string, roles: Role[]): Promise<string[]> {\n if (roles.length === 0) {\n return [];\n }\n const rows = await this.db\n .select({ id: orgUsers.id })\n .from(orgUsers)\n .where(\n and(\n eq(orgUsers.orgId, orgId),\n eq(orgUsers.status, \"active\"),\n inArray(orgUsers.role, roles),\n ),\n )\n .orderBy(orgUsers.id);\n return rows.map((row) => row.id);\n }\n\n async createOrgUser(input: CreateOrgUserInput): Promise<OrgUser> {\n try {\n const [row] = await this.db\n .insert(orgUsers)\n .values({\n orgId: input.orgId,\n userId: input.userId,\n role: input.role,\n status: input.status ?? \"active\",\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert org user\");\n }\n return { ...row, role: parseRole(row.role) };\n } catch (err) {\n if (isUniqueViolation(err)) {\n throw new ConflictError(\"That account already belongs to this organization\");\n }\n throw err;\n }\n }\n\n async updateOrgUser(\n orgId: string,\n id: string,\n patch: UpdateOrgUserInput,\n ): Promise<OrgUser | null> {\n const [row] = await this.db\n .update(orgUsers)\n .set({\n ...(patch.role !== undefined && { role: patch.role }),\n ...(patch.status !== undefined && { status: patch.status }),\n })\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .returning();\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findStudentOrgUsers(userId: string): Promise<OrgUser[]> {\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.userId, userId), eq(orgUsers.role, \"student\")))\n .orderBy(orgUsers.createdAt);\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async deleteOrgUser(orgId: string, id: string): Promise<OrgUser | null> {\n try {\n const [row] = await this.db\n .delete(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .returning();\n return row ?? null;\n } catch (err) {\n if (pgErrorFields(err)?.code === \"23503\") {\n throw Object.assign(\n new ConflictError(\n \"This person has written posts, comments or messages and cannot be deleted\",\n ),\n { cause: err },\n );\n }\n throw err;\n }\n }\n}\ntranslateDbErrors(DrizzleOrganizationsRepository);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"organizations.js","names":[],"sources":["../../src/repositories/organizations.ts"],"sourcesContent":["// organizations — Drizzle repository (implements the core outbound port).\nimport { and, asc, eq, inArray, sql } from \"drizzle-orm\";\nimport type { NodePgDatabase } from \"drizzle-orm/node-postgres\";\nimport {\n parseRole,\n type CreateOrgUserInput,\n type Invite,\n type NewInviteRow,\n type NewOrganizationRow,\n type Organization,\n type OrganizationsRepository,\n type OrgUser,\n type Role,\n type UpdateOrganizationInput,\n type UpdateOrgUserInput,\n} from \"@headless-lms/core/organizations\";\nimport { invites, organizations, orgUsers } from \"../schema/organizations.js\";\nimport type { Logger } from \"@headless-lms/core/shared/ports\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport { ConflictError } from \"@headless-lms/core/shared/errors\";\nimport { isUniqueViolation, pgErrorFields, translateDbErrors } from \"./pg-errors.js\";\n\nconst INVITE_STATUSES = [\"pending\", \"accepted\", \"rejected\", \"canceled\"] as const;\ntype InviteStatus = (typeof INVITE_STATUSES)[number];\nconst toStatus = (s: string): InviteStatus =>\n (INVITE_STATUSES as readonly string[]).includes(s) ? (s as InviteStatus) : \"pending\";\n\nfunction toInvite(row: typeof invites.$inferSelect): Invite {\n return {\n id: row.id,\n orgId: row.orgId,\n email: row.email,\n role: row.role,\n status: row.status,\n invitedBy: row.invitedBy,\n tokenHash: row.tokenHash,\n expiresAt: row.expiresAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nexport class DrizzleOrganizationsRepository implements OrganizationsRepository {\n constructor(\n private readonly db: NodePgDatabase,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async create(input: NewOrganizationRow): Promise<Organization> {\n const [row] = await this.db\n .insert(organizations)\n .values({\n externalId: input.externalId,\n name: input.name,\n slug: input.slug,\n ownerId: input.ownerId,\n id: input.id,\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert organization\");\n }\n return row!;\n }\n\n async update(id: string, input: UpdateOrganizationInput): Promise<Organization | null> {\n const [row] = await this.db\n .update(organizations)\n .set({ name: input.name, slug: input.slug })\n .where(eq(organizations.id, id))\n .returning();\n return row ?? null;\n }\n\n async delete(id: string): Promise<Organization> {\n const [row] = await this.db.delete(organizations).where(eq(organizations.id, id)).returning();\n\n if (!row) {\n throw new Error(\"failed to delete organization\");\n }\n return row;\n }\n\n async findById(id: string): Promise<Organization | null> {\n const [row] = await this.db.select().from(organizations).where(eq(organizations.id, id));\n return row ?? null;\n }\n\n async listIds(): Promise<string[]> {\n const rows = await this.db\n .select({ id: organizations.id })\n .from(organizations)\n .orderBy(asc(organizations.id));\n return rows.map((row) => row.id);\n }\n\n async findByExternalId(externalId: string): Promise<Organization | null> {\n const [row] = await this.db\n .select()\n .from(organizations)\n .where(eq(organizations.externalId, externalId))\n .limit(1);\n return row ?? null;\n }\n\n async findBySlug(slug: string): Promise<Organization | null> {\n const [row] = await this.db\n .select()\n .from(organizations)\n .where(eq(organizations.slug, slug))\n .limit(1);\n return row ?? null;\n }\n\n async upsertPendingInvite(orgId: string, input: NewInviteRow): Promise<Invite> {\n const [row] = await this.db\n .insert(invites)\n .values({\n orgId,\n email: input.email,\n role: input.role,\n status: \"pending\",\n invitedBy: input.invitedBy,\n tokenHash: input.tokenHash,\n expiresAt: input.expiresAt,\n })\n .onConflictDoUpdate({\n target: [invites.orgId, invites.email],\n targetWhere: sql`${invites.status} = 'pending'`,\n set: {\n role: input.role,\n invitedBy: input.invitedBy,\n tokenHash: input.tokenHash,\n expiresAt: input.expiresAt,\n },\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to upsert invite\");\n }\n return toInvite(row);\n }\n\n async setInviteStatus(orgId: string, id: string, status: string): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ status: toStatus(status) })\n .where(and(eq(invites.orgId, orgId), eq(invites.id, id)))\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async findInviteByTokenHash(tokenHash: string): Promise<Invite | null> {\n const [row] = await this.db\n .select()\n .from(invites)\n .where(eq(invites.tokenHash, tokenHash))\n .limit(1);\n return row ? toInvite(row) : null;\n }\n\n async findPendingInvite(orgId: string, email: string): Promise<Invite | null> {\n const [row] = await this.db\n .select()\n .from(invites)\n .where(\n and(\n eq(invites.orgId, orgId),\n sql`lower(${invites.email}) = lower(${email})`,\n eq(invites.status, \"pending\"),\n ),\n )\n .limit(1);\n return row ? toInvite(row) : null;\n }\n\n async setInviteToken(\n orgId: string,\n id: string,\n tokenHash: string,\n expiresAt: Date,\n ): Promise<Invite | null> {\n const [row] = await this.db\n .update(invites)\n .set({ tokenHash, expiresAt })\n .where(and(eq(invites.orgId, orgId), eq(invites.id, id), eq(invites.status, \"pending\")))\n .returning();\n return row ? toInvite(row) : null;\n }\n\n async findOrgUser(orgId: string, userId: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.userId, userId)))\n .limit(1);\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findOrgUsersByUser(userId: string): Promise<OrgUser[]> {\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(eq(orgUsers.userId, userId))\n .orderBy(orgUsers.createdAt);\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async findOrgUserById(orgId: string, id: string): Promise<OrgUser | null> {\n const [row] = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .limit(1);\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findOrgUsersByIds(orgId: string, ids: string[]): Promise<OrgUser[]> {\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), inArray(orgUsers.id, ids)));\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async findOrgUserIdsByRole(orgId: string, roles: Role[]): Promise<string[]> {\n if (roles.length === 0) {\n return [];\n }\n const rows = await this.db\n .select({ id: orgUsers.id })\n .from(orgUsers)\n .where(\n and(\n eq(orgUsers.orgId, orgId),\n eq(orgUsers.status, \"active\"),\n inArray(orgUsers.role, roles),\n ),\n )\n .orderBy(orgUsers.id);\n return rows.map((row) => row.id);\n }\n\n async createOrgUser(input: CreateOrgUserInput): Promise<OrgUser> {\n try {\n const [row] = await this.db\n .insert(orgUsers)\n .values({\n orgId: input.orgId,\n userId: input.userId,\n role: input.role,\n status: input.status ?? \"active\",\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert org user\");\n }\n return { ...row, role: parseRole(row.role) };\n } catch (err) {\n if (isUniqueViolation(err)) {\n throw new ConflictError(\"That account already belongs to this organization\");\n }\n throw err;\n }\n }\n\n async updateOrgUser(\n orgId: string,\n id: string,\n patch: UpdateOrgUserInput,\n ): Promise<OrgUser | null> {\n const [row] = await this.db\n .update(orgUsers)\n .set({\n ...(patch.role !== undefined && { role: patch.role }),\n ...(patch.status !== undefined && { status: patch.status }),\n })\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .returning();\n return row ? { ...row, role: parseRole(row.role) } : null;\n }\n\n async findStudentOrgUsers(userId: string): Promise<OrgUser[]> {\n const rows = await this.db\n .select()\n .from(orgUsers)\n .where(and(eq(orgUsers.userId, userId), eq(orgUsers.role, \"student\")))\n .orderBy(orgUsers.createdAt);\n return rows.map((row) => ({ ...row, role: parseRole(row.role) }));\n }\n\n async deleteOrgUser(orgId: string, id: string): Promise<OrgUser | null> {\n try {\n const [row] = await this.db\n .delete(orgUsers)\n .where(and(eq(orgUsers.orgId, orgId), eq(orgUsers.id, id)))\n .returning();\n return row ?? null;\n } catch (err) {\n if (pgErrorFields(err)?.code === \"23503\") {\n throw Object.assign(\n new ConflictError(\n \"This person has written posts, comments or messages and cannot be deleted\",\n ),\n { cause: err },\n );\n }\n throw err;\n }\n }\n}\ntranslateDbErrors(DrizzleOrganizationsRepository);\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
2
|
import { Logger, Page, Post, PostFlag, PostFlagResolution, ReactionEmoji } from "@headless-lms/core/types";
|
|
3
|
-
import { BookmarkQuery, FeedQuery, PageQuery, PostPatch, PostReactions, PostRepository } from "@headless-lms/core/discussion";
|
|
3
|
+
import { BookmarkQuery, FeedQuery, PageQuery, PostGuard, PostPatch, PostReactions, PostRepository, PublishedPost } from "@headless-lms/core/discussion";
|
|
4
4
|
//#region src/repositories/posts.d.ts
|
|
5
5
|
declare class DrizzlePostRepository implements PostRepository {
|
|
6
6
|
private readonly db;
|
|
@@ -8,20 +8,16 @@ declare class DrizzlePostRepository implements PostRepository {
|
|
|
8
8
|
constructor(db: NodePgDatabase, logger?: Logger);
|
|
9
9
|
insert(orgId: string, post: Post): Promise<Post>;
|
|
10
10
|
update(orgId: string, id: string, patch: PostPatch): Promise<Post | null>;
|
|
11
|
+
updateIf(orgId: string, id: string, guard: PostGuard, patch: PostPatch): Promise<Post | null>;
|
|
11
12
|
find(orgId: string, id: string): Promise<Post | null>;
|
|
12
13
|
findMany(orgId: string, ids: string[]): Promise<Post[]>;
|
|
14
|
+
publishedBetween(orgId: string, communityId: string, from: Date, to: Date): Promise<PublishedPost[]>;
|
|
13
15
|
listPublished(orgId: string, spaceIds: string[], query: FeedQuery): Promise<Page<Post>>;
|
|
14
16
|
listByAuthor(orgId: string, orgUserId: string, spaceIds: string[], query: PageQuery): Promise<Page<Post>>;
|
|
15
17
|
listBookmarked(orgId: string, orgUserId: string, spaceIds: string[], query: BookmarkQuery): Promise<Page<Post>>;
|
|
16
18
|
listPending(orgId: string, spaceId: string, limit: number): Promise<Post[]>;
|
|
17
|
-
listDue(orgId: string, now: Date, limit: number, afterId?: string): Promise<Post[]>;
|
|
18
|
-
hasDue(orgId: string, now: Date): Promise<boolean>;
|
|
19
19
|
reactionsOf(orgId: string, postIds: string[], viewerId: string): Promise<Record<string, PostReactions>>;
|
|
20
|
-
setReaction(orgId: string, postId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<
|
|
21
|
-
follow(orgId: string, postId: string, orgUserId: string): Promise<void>;
|
|
22
|
-
followerIds(orgId: string, postId: string): Promise<string[]>;
|
|
23
|
-
followedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]>;
|
|
24
|
-
unfollow(orgId: string, postId: string, orgUserId: string): Promise<void>;
|
|
20
|
+
setReaction(orgId: string, postId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<boolean>;
|
|
25
21
|
bookmark(orgId: string, postId: string, orgUserId: string): Promise<void>;
|
|
26
22
|
unbookmark(orgId: string, postId: string, orgUserId: string): Promise<void>;
|
|
27
23
|
bookmarkedAmong(orgId: string, postIds: string[], orgUserId: string): Promise<string[]>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { postBookmarks, postFlags,
|
|
1
|
+
import { comments, postBookmarks, postFlags, postReactions, posts } from "../schema/discussion.js";
|
|
2
2
|
import "../schema/index.js";
|
|
3
3
|
import { translateDbErrors } from "./pg-errors.js";
|
|
4
|
+
import { insertedRow } from "./inserted-row.js";
|
|
4
5
|
import { and, asc, desc, eq, gt, inArray, isNull, lte, or, sql } from "drizzle-orm";
|
|
5
6
|
import { noopLogger } from "@headless-lms/core/shared/logger";
|
|
6
7
|
//#region src/repositories/posts.ts
|
|
@@ -83,6 +84,13 @@ var DrizzlePostRepository = class {
|
|
|
83
84
|
}).where(and(eq(posts.orgId, orgId), eq(posts.id, id))).returning();
|
|
84
85
|
return row ? toPost(row) : null;
|
|
85
86
|
}
|
|
87
|
+
async updateIf(orgId, id, guard, patch) {
|
|
88
|
+
const [row] = await this.db.update(posts).set({
|
|
89
|
+
...patch,
|
|
90
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
91
|
+
}).where(and(eq(posts.orgId, orgId), eq(posts.id, id), eq(posts.status, guard.status), guard.scheduledAt === void 0 ? void 0 : eq(posts.scheduledAt, guard.scheduledAt), guard.dueBy === void 0 ? void 0 : lte(posts.scheduledAt, guard.dueBy))).returning();
|
|
92
|
+
return row ? toPost(row) : null;
|
|
93
|
+
}
|
|
86
94
|
async find(orgId, id) {
|
|
87
95
|
const [row] = await this.db.select().from(posts).where(and(eq(posts.orgId, orgId), eq(posts.id, id))).limit(1);
|
|
88
96
|
return row ? toPost(row) : null;
|
|
@@ -91,6 +99,18 @@ var DrizzlePostRepository = class {
|
|
|
91
99
|
if (ids.length === 0) return [];
|
|
92
100
|
return (await this.db.select().from(posts).where(and(eq(posts.orgId, orgId), inArray(posts.id, ids)))).map(toPost);
|
|
93
101
|
}
|
|
102
|
+
async publishedBetween(orgId, communityId, from, to) {
|
|
103
|
+
return (await this.db.select({
|
|
104
|
+
postId: posts.id,
|
|
105
|
+
spaceId: posts.spaceId,
|
|
106
|
+
title: posts.title,
|
|
107
|
+
orgUserId: posts.orgUserId,
|
|
108
|
+
commentCount: sql`(select count(*)::int from ${comments} where ${comments.orgId} = ${posts.orgId} and ${comments.postId} = ${posts.id} and ${comments.status} = 'published')`
|
|
109
|
+
}).from(posts).where(and(eq(posts.orgId, orgId), eq(posts.communityId, communityId), eq(posts.status, "published"), gt(posts.publishedAt, from), lte(posts.publishedAt, to))).orderBy(asc(posts.publishedAt), asc(posts.id))).map((row) => ({
|
|
110
|
+
...row,
|
|
111
|
+
commentCount: Number(row.commentCount)
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
94
114
|
async listPublished(orgId, spaceIds, query) {
|
|
95
115
|
if (spaceIds.length === 0) return {
|
|
96
116
|
rows: [],
|
|
@@ -145,13 +165,6 @@ var DrizzlePostRepository = class {
|
|
|
145
165
|
async listPending(orgId, spaceId, limit) {
|
|
146
166
|
return (await this.db.select().from(posts).where(and(eq(posts.orgId, orgId), eq(posts.spaceId, spaceId), eq(posts.status, "pending"))).orderBy(asc(posts.createdAt), asc(posts.id)).limit(limit)).map(toPost);
|
|
147
167
|
}
|
|
148
|
-
async listDue(orgId, now, limit, afterId) {
|
|
149
|
-
return (await this.db.select().from(posts).where(and(eq(posts.orgId, orgId), eq(posts.status, "scheduled"), lte(posts.scheduledAt, now), afterId === void 0 ? void 0 : gt(posts.id, afterId))).orderBy(asc(posts.id)).limit(limit)).map(toPost);
|
|
150
|
-
}
|
|
151
|
-
async hasDue(orgId, now) {
|
|
152
|
-
const due = this.db.select({ id: posts.id }).from(posts).where(and(eq(posts.orgId, orgId), eq(posts.status, "scheduled"), lte(posts.scheduledAt, now))).limit(1);
|
|
153
|
-
return (await this.db.execute(sql`select exists (${due}) as due`)).rows[0]?.due === true;
|
|
154
|
-
}
|
|
155
168
|
async reactionsOf(orgId, postIds, viewerId) {
|
|
156
169
|
if (postIds.length === 0) return {};
|
|
157
170
|
const rows = await this.db.select({
|
|
@@ -171,9 +184,9 @@ var DrizzlePostRepository = class {
|
|
|
171
184
|
async setReaction(orgId, postId, orgUserId, emoji) {
|
|
172
185
|
if (emoji === null) {
|
|
173
186
|
await this.db.delete(postReactions).where(and(eq(postReactions.orgId, orgId), eq(postReactions.postId, postId), eq(postReactions.orgUserId, orgUserId)));
|
|
174
|
-
return;
|
|
187
|
+
return false;
|
|
175
188
|
}
|
|
176
|
-
await this.db.insert(postReactions).values({
|
|
189
|
+
const [row] = await this.db.insert(postReactions).values({
|
|
177
190
|
orgId,
|
|
178
191
|
postId,
|
|
179
192
|
orgUserId,
|
|
@@ -185,24 +198,8 @@ var DrizzlePostRepository = class {
|
|
|
185
198
|
postReactions.orgUserId
|
|
186
199
|
],
|
|
187
200
|
set: { emoji }
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
async follow(orgId, postId, orgUserId) {
|
|
191
|
-
await this.db.insert(postFollows).values({
|
|
192
|
-
orgId,
|
|
193
|
-
postId,
|
|
194
|
-
orgUserId
|
|
195
|
-
}).onConflictDoNothing();
|
|
196
|
-
}
|
|
197
|
-
async followerIds(orgId, postId) {
|
|
198
|
-
return (await this.db.select({ orgUserId: postFollows.orgUserId }).from(postFollows).where(and(eq(postFollows.orgId, orgId), eq(postFollows.postId, postId)))).map((row) => row.orgUserId);
|
|
199
|
-
}
|
|
200
|
-
async followedAmong(orgId, postIds, orgUserId) {
|
|
201
|
-
if (postIds.length === 0) return [];
|
|
202
|
-
return (await this.db.select({ postId: postFollows.postId }).from(postFollows).where(and(eq(postFollows.orgId, orgId), eq(postFollows.orgUserId, orgUserId), inArray(postFollows.postId, postIds)))).map((row) => row.postId);
|
|
203
|
-
}
|
|
204
|
-
async unfollow(orgId, postId, orgUserId) {
|
|
205
|
-
await this.db.delete(postFollows).where(and(eq(postFollows.orgId, orgId), eq(postFollows.postId, postId), eq(postFollows.orgUserId, orgUserId)));
|
|
201
|
+
}).returning({ inserted: insertedRow });
|
|
202
|
+
return row?.inserted === true;
|
|
206
203
|
}
|
|
207
204
|
async bookmark(orgId, postId, orgUserId) {
|
|
208
205
|
await this.db.insert(postBookmarks).values({
|