@headless-lms/adapter-db 0.4.0 → 0.6.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.
Files changed (94) hide show
  1. package/dist/client.d.ts +8 -2
  2. package/dist/client.js +10 -2
  3. package/dist/client.js.map +1 -1
  4. package/dist/index.d.ts +5 -4
  5. package/dist/index.js +4 -3
  6. package/dist/repositories/admin.d.ts +12 -4
  7. package/dist/repositories/admin.js +164 -22
  8. package/dist/repositories/admin.js.map +1 -1
  9. package/dist/repositories/assets.d.ts +2 -1
  10. package/dist/repositories/assets.js +12 -1
  11. package/dist/repositories/assets.js.map +1 -1
  12. package/dist/repositories/automations.js +1 -1
  13. package/dist/repositories/chats.d.ts +13 -4
  14. package/dist/repositories/chats.js +164 -70
  15. package/dist/repositories/chats.js.map +1 -1
  16. package/dist/repositories/content.d.ts +22 -5
  17. package/dist/repositories/content.js +201 -40
  18. package/dist/repositories/content.js.map +1 -1
  19. package/dist/repositories/credentials.js +1 -1
  20. package/dist/repositories/devices.js +1 -1
  21. package/dist/repositories/digest-runs.js +1 -1
  22. package/dist/repositories/discussion.d.ts +1 -1
  23. package/dist/repositories/discussion.js +9 -3
  24. package/dist/repositories/discussion.js.map +1 -1
  25. package/dist/repositories/entitlements.d.ts +10 -10
  26. package/dist/repositories/entitlements.js +47 -56
  27. package/dist/repositories/entitlements.js.map +1 -1
  28. package/dist/repositories/event-reports.d.ts +16 -0
  29. package/dist/repositories/event-reports.js +199 -0
  30. package/dist/repositories/event-reports.js.map +1 -0
  31. package/dist/repositories/grant-covers.d.ts +4 -3
  32. package/dist/repositories/grant-covers.js +7 -4
  33. package/dist/repositories/grant-covers.js.map +1 -1
  34. package/dist/repositories/identity.js +1 -1
  35. package/dist/repositories/integrations.js +1 -1
  36. package/dist/repositories/member.d.ts +12 -4
  37. package/dist/repositories/member.js +19 -7
  38. package/dist/repositories/member.js.map +1 -1
  39. package/dist/repositories/members.d.ts +2 -5
  40. package/dist/repositories/members.js +39 -94
  41. package/dist/repositories/members.js.map +1 -1
  42. package/dist/repositories/mentions.js +1 -1
  43. package/dist/repositories/notification-settings.js +1 -1
  44. package/dist/repositories/notifications.js +2 -2
  45. package/dist/repositories/notifications.js.map +1 -1
  46. package/dist/repositories/org-user-profile.d.ts +0 -17
  47. package/dist/repositories/org-user-profile.js +0 -1
  48. package/dist/repositories/org-user-profile.js.map +1 -1
  49. package/dist/repositories/organizations.d.ts +4 -4
  50. package/dist/repositories/organizations.js +26 -46
  51. package/dist/repositories/organizations.js.map +1 -1
  52. package/dist/repositories/outbox.d.ts +5 -1
  53. package/dist/repositories/outbox.js +12 -3
  54. package/dist/repositories/outbox.js.map +1 -1
  55. package/dist/repositories/posts.js +1 -1
  56. package/dist/repositories/progress.js +1 -1
  57. package/dist/repositories/roles.d.ts +35 -0
  58. package/dist/repositories/roles.js +244 -0
  59. package/dist/repositories/roles.js.map +1 -0
  60. package/dist/repositories/settings.js +1 -1
  61. package/dist/repositories/subscriptions.js +1 -1
  62. package/dist/schema/assets.d.ts +17 -0
  63. package/dist/schema/assets.js +1 -0
  64. package/dist/schema/assets.js.map +1 -1
  65. package/dist/schema/automations.d.ts +3 -3
  66. package/dist/schema/automations.js +1 -1
  67. package/dist/schema/automations.js.map +1 -1
  68. package/dist/schema/content.d.ts +488 -3
  69. package/dist/schema/content.js +89 -4
  70. package/dist/schema/content.js.map +1 -1
  71. package/dist/schema/discussion.d.ts +17 -0
  72. package/dist/schema/discussion.js +4 -2
  73. package/dist/schema/discussion.js.map +1 -1
  74. package/dist/schema/index.d.ts +4 -3
  75. package/dist/schema/index.js +7 -2
  76. package/dist/schema/index.js.map +1 -1
  77. package/dist/schema/organizations.d.ts +24 -22
  78. package/dist/schema/organizations.js +3 -14
  79. package/dist/schema/organizations.js.map +1 -1
  80. package/dist/schema/roles.d.ts +369 -0
  81. package/dist/schema/roles.js +71 -0
  82. package/dist/schema/roles.js.map +1 -0
  83. package/drizzle/0017_configurable_roles.sql +38 -0
  84. package/drizzle/0018_messages_client_id.sql +2 -0
  85. package/drizzle/0019_events.sql +44 -0
  86. package/drizzle/0020_space_images.sql +9 -0
  87. package/drizzle/0021_org_metadata.sql +1 -0
  88. package/drizzle/meta/0017_snapshot.json +9518 -0
  89. package/drizzle/meta/0018_snapshot.json +9557 -0
  90. package/drizzle/meta/0019_snapshot.json +9919 -0
  91. package/drizzle/meta/0020_snapshot.json +10045 -0
  92. package/drizzle/meta/0021_snapshot.json +10052 -0
  93. package/drizzle/meta/_journal.json +35 -0
  94. package/package.json +2 -2
@@ -1,8 +1,9 @@
1
1
  import { assets } from "../schema/assets.js";
2
+ import { contentItems } from "../schema/content.js";
2
3
  import { pgErrorFields, translateDbErrors } from "./pg-errors.js";
4
+ import { noopLogger } from "@headless-lms/core/shared/logger";
3
5
  import { and, asc, desc, eq, ilike, inArray, lt, sql } from "drizzle-orm";
4
6
  import { ConflictError } from "@headless-lms/core/shared/errors";
5
- import { noopLogger } from "@headless-lms/core/shared/logger";
6
7
  //#region src/repositories/assets.ts
7
8
  function toAsset(row) {
8
9
  return {
@@ -16,6 +17,7 @@ function toAsset(row) {
16
17
  size: row.size,
17
18
  status: row.status,
18
19
  uploadedBy: row.uploadedBy,
20
+ contentId: row.contentId,
19
21
  createdAt: row.createdAt,
20
22
  updatedAt: row.updatedAt
21
23
  };
@@ -35,6 +37,9 @@ function referenced(orgId, assetId) {
35
37
  or exists (select 1 from courses c where c.org_id = ${orgId} and c.thumbnail_asset_id = ${assetId})
36
38
  or exists (select 1 from downloads d where d.org_id = ${orgId} and d.thumbnail_asset_id = ${assetId})
37
39
  or exists (select 1 from communities c where c.org_id = ${orgId} and c.thumbnail_asset_id = ${assetId})
40
+ or exists (select 1 from spaces s where s.org_id = ${orgId} and ${assetId} in (s.cover_asset_id, s.background_asset_id, s.icon_asset_id))
41
+ or exists (select 1 from event_assets ea where ea.org_id = ${orgId} and ea.asset_id = ${assetId})
42
+ or exists (select 1 from events e where e.org_id = ${orgId} and e.thumbnail_asset_id = ${assetId})
38
43
  )`;
39
44
  }
40
45
  function unreferencedMemberUploads(orgId, before) {
@@ -60,6 +65,7 @@ var DrizzleAssetsRepository = class {
60
65
  size: asset.size,
61
66
  status: asset.status,
62
67
  uploadedBy: asset.uploadedBy,
68
+ contentId: asset.contentId,
63
69
  createdAt: new Date(asset.createdAt)
64
70
  }).returning();
65
71
  if (!row) throw new Error("failed to insert asset");
@@ -69,6 +75,7 @@ var DrizzleAssetsRepository = class {
69
75
  const filters = [eq(assets.orgId, orgId)];
70
76
  if (query.kind) filters.push(eq(assets.kind, query.kind));
71
77
  if (query.source) filters.push(eq(assets.source, query.source));
78
+ if (query.contentId) filters.push(eq(assets.contentId, query.contentId));
72
79
  const q = query.search?.trim();
73
80
  if (q) filters.push(ilike(assets.filename, `%${q}%`));
74
81
  const where = and(...filters);
@@ -122,6 +129,10 @@ var DrizzleAssetsRepository = class {
122
129
  if (keys.length === 0) return [];
123
130
  return (await this.db.select({ key: assets.key }).from(assets).where(and(eq(assets.orgId, orgId), inArray(assets.key, keys)))).map((r) => r.key);
124
131
  }
132
+ async contentTypeOf(orgId, contentId) {
133
+ const [row] = await this.db.select({ type: contentItems.type }).from(contentItems).where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, contentId))).limit(1);
134
+ return row?.type ?? null;
135
+ }
125
136
  };
126
137
  translateDbErrors(DrizzleAssetsRepository);
127
138
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"assets.js","names":[],"sources":["../../src/repositories/assets.ts"],"sourcesContent":["import { and, eq, ilike, inArray, lt, sql, asc, desc, type SQL } from \"drizzle-orm\";\nimport type { AssetsRepository } from \"@headless-lms/core/assets\";\nimport type {\n Asset,\n AssetKind,\n AssetSource,\n AssetStatus,\n AssetsQuery,\n Logger,\n Page,\n} from \"@headless-lms/core/types\";\nimport { assets } from \"../schema/assets.js\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport type { DbExecutor } from \"../client.js\";\nimport { ConflictError } from \"@headless-lms/core/shared/errors\";\nimport { pgErrorFields, translateDbErrors } from \"./pg-errors.js\";\n\ntype Row = typeof assets.$inferSelect;\n\nfunction toAsset(row: Row): Asset {\n return {\n orgId: row.orgId,\n id: row.id,\n key: row.key,\n kind: row.kind as AssetKind,\n source: row.source as AssetSource,\n filename: row.filename,\n contentType: row.contentType,\n size: row.size,\n status: row.status as AssetStatus,\n uploadedBy: row.uploadedBy,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nconst SORT_COLUMNS = {\n filename: assets.filename,\n size: assets.size,\n createdAt: assets.createdAt,\n} as const;\n\nfunction referenced(orgId: SQL, assetId: SQL): SQL {\n return sql`(\n exists (select 1 from user_profiles p where p.image_asset_org_id = ${orgId} and p.image_asset_id = ${assetId})\n or exists (select 1 from posts p where p.org_id = ${orgId} and p.cover_asset_id = ${assetId})\n or exists (select 1 from message_attachments ma where ma.org_id = ${orgId} and ma.asset_id = ${assetId})\n or exists (select 1 from activity_assets aa where aa.org_id = ${orgId} and aa.asset_id = ${assetId})\n or exists (select 1 from download_assets da where da.org_id = ${orgId} and da.asset_id = ${assetId})\n or exists (select 1 from courses c where c.org_id = ${orgId} and c.thumbnail_asset_id = ${assetId})\n or exists (select 1 from downloads d where d.org_id = ${orgId} and d.thumbnail_asset_id = ${assetId})\n or exists (select 1 from communities c where c.org_id = ${orgId} and c.thumbnail_asset_id = ${assetId})\n )`;\n}\n\nfunction unreferencedMemberUploads(orgId: string, before: Date): SQL | undefined {\n return and(\n eq(assets.orgId, orgId),\n eq(assets.source, \"member\"),\n lt(assets.createdAt, before),\n sql`not ${referenced(sql`${assets.orgId}`, sql`${assets.id}`)}`,\n );\n}\n\nexport class DrizzleAssetsRepository implements AssetsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, asset: Asset): Promise<Asset> {\n if (asset.orgId !== orgId) {\n throw new Error(\"asset org mismatch\");\n }\n const [row] = await this.db\n .insert(assets)\n .values({\n id: asset.id,\n orgId: asset.orgId,\n key: asset.key,\n kind: asset.kind,\n source: asset.source,\n filename: asset.filename,\n contentType: asset.contentType,\n size: asset.size,\n status: asset.status,\n uploadedBy: asset.uploadedBy,\n createdAt: new Date(asset.createdAt),\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert asset\");\n }\n return toAsset(row);\n }\n\n async list(orgId: string, query: AssetsQuery): Promise<Page<Asset>> {\n const filters: (SQL | undefined)[] = [eq(assets.orgId, orgId)];\n if (query.kind) {\n filters.push(eq(assets.kind, query.kind));\n }\n if (query.source) {\n filters.push(eq(assets.source, query.source));\n }\n const q = query.search?.trim();\n if (q) {\n filters.push(ilike(assets.filename, `%${q}%`));\n }\n const where = and(...filters);\n\n const orderBy = (() => {\n if (!query.sort) {\n return desc(assets.createdAt);\n }\n const descending = query.sort.startsWith(\"-\");\n const field = descending ? query.sort.slice(1) : query.sort;\n const col = SORT_COLUMNS[field as keyof typeof SORT_COLUMNS] ?? assets.createdAt;\n return descending ? desc(col) : asc(col);\n })();\n\n const offset = (query.page - 1) * query.pageSize;\n const rows = await this.db\n .select()\n .from(assets)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset(offset);\n\n const counted = await this.db\n .select({ value: sql<number>`count(*)::int` })\n .from(assets)\n .where(where);\n\n return {\n rows: rows.map(toAsset),\n total: counted[0]?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findById(orgId: string, id: string): Promise<Asset | null> {\n const [row] = await this.db\n .select()\n .from(assets)\n .where(and(eq(assets.orgId, orgId), eq(assets.id, id)))\n .limit(1);\n return row ? toAsset(row) : null;\n }\n\n async findByIds(orgId: string, ids: string[]): Promise<Asset[]> {\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(assets)\n .where(and(eq(assets.orgId, orgId), inArray(assets.id, ids)));\n return rows.map(toAsset);\n }\n\n async markReady(\n orgId: string,\n id: string,\n patch: Pick<Asset, \"key\" | \"kind\" | \"size\" | \"contentType\">,\n ): Promise<Asset | null> {\n const [row] = await this.db\n .update(assets)\n .set({\n key: patch.key,\n kind: patch.kind,\n size: patch.size,\n contentType: patch.contentType,\n status: \"ready\",\n })\n .where(and(eq(assets.orgId, orgId), eq(assets.id, id), eq(assets.status, \"pending\")))\n .returning();\n return row ? toAsset(row) : null;\n }\n\n async delete(orgId: string, id: string): Promise<boolean> {\n try {\n const deleted = await this.db\n .delete(assets)\n .where(and(eq(assets.orgId, orgId), eq(assets.id, id)))\n .returning({ id: assets.id });\n return deleted.length > 0;\n } catch (err) {\n if (pgErrorFields(err)?.code === \"23503\") {\n throw Object.assign(new ConflictError(\"This file is still in use\"), { cause: err });\n }\n throw err;\n }\n }\n\n async listUnreferencedMemberUploads(orgId: string, before: Date): Promise<Asset[]> {\n const rows = await this.db\n .select()\n .from(assets)\n .where(unreferencedMemberUploads(orgId, before))\n .orderBy(asc(assets.createdAt));\n return rows.map(toAsset);\n }\n\n async heldKeys(orgId: string, keys: string[]): Promise<string[]> {\n if (keys.length === 0) {\n return [];\n }\n const rows = await this.db\n .select({ key: assets.key })\n .from(assets)\n .where(and(eq(assets.orgId, orgId), inArray(assets.key, keys)));\n return rows.map((r) => r.key);\n }\n}\ntranslateDbErrors(DrizzleAssetsRepository);\n"],"mappings":"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"}
1
+ {"version":3,"file":"assets.js","names":[],"sources":["../../src/repositories/assets.ts"],"sourcesContent":["import { and, eq, ilike, inArray, lt, sql, asc, desc, type SQL } from \"drizzle-orm\";\nimport type { AssetsRepository } from \"@headless-lms/core/assets\";\nimport type {\n Asset,\n AssetKind,\n AssetSource,\n AssetStatus,\n AssetsQuery,\n ContentType,\n Logger,\n Page,\n} from \"@headless-lms/core/types\";\nimport { assets } from \"../schema/assets.js\";\nimport { contentItems } from \"../schema/content.js\";\nimport { noopLogger } from \"@headless-lms/core/shared/logger\";\nimport type { DbExecutor } from \"../client.js\";\nimport { ConflictError } from \"@headless-lms/core/shared/errors\";\nimport { pgErrorFields, translateDbErrors } from \"./pg-errors.js\";\n\ntype Row = typeof assets.$inferSelect;\n\nfunction toAsset(row: Row): Asset {\n return {\n orgId: row.orgId,\n id: row.id,\n key: row.key,\n kind: row.kind as AssetKind,\n source: row.source as AssetSource,\n filename: row.filename,\n contentType: row.contentType,\n size: row.size,\n status: row.status as AssetStatus,\n uploadedBy: row.uploadedBy,\n contentId: row.contentId,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nconst SORT_COLUMNS = {\n filename: assets.filename,\n size: assets.size,\n createdAt: assets.createdAt,\n} as const;\n\nfunction referenced(orgId: SQL, assetId: SQL): SQL {\n return sql`(\n exists (select 1 from user_profiles p where p.image_asset_org_id = ${orgId} and p.image_asset_id = ${assetId})\n or exists (select 1 from posts p where p.org_id = ${orgId} and p.cover_asset_id = ${assetId})\n or exists (select 1 from message_attachments ma where ma.org_id = ${orgId} and ma.asset_id = ${assetId})\n or exists (select 1 from activity_assets aa where aa.org_id = ${orgId} and aa.asset_id = ${assetId})\n or exists (select 1 from download_assets da where da.org_id = ${orgId} and da.asset_id = ${assetId})\n or exists (select 1 from courses c where c.org_id = ${orgId} and c.thumbnail_asset_id = ${assetId})\n or exists (select 1 from downloads d where d.org_id = ${orgId} and d.thumbnail_asset_id = ${assetId})\n or exists (select 1 from communities c where c.org_id = ${orgId} and c.thumbnail_asset_id = ${assetId})\n or exists (select 1 from spaces s where s.org_id = ${orgId} and ${assetId} in (s.cover_asset_id, s.background_asset_id, s.icon_asset_id))\n or exists (select 1 from event_assets ea where ea.org_id = ${orgId} and ea.asset_id = ${assetId})\n or exists (select 1 from events e where e.org_id = ${orgId} and e.thumbnail_asset_id = ${assetId})\n )`;\n}\n\nfunction unreferencedMemberUploads(orgId: string, before: Date): SQL | undefined {\n return and(\n eq(assets.orgId, orgId),\n eq(assets.source, \"member\"),\n lt(assets.createdAt, before),\n sql`not ${referenced(sql`${assets.orgId}`, sql`${assets.id}`)}`,\n );\n}\n\nexport class DrizzleAssetsRepository implements AssetsRepository {\n constructor(\n private readonly db: DbExecutor,\n private readonly logger: Logger = noopLogger,\n ) {}\n\n async insert(orgId: string, asset: Asset): Promise<Asset> {\n if (asset.orgId !== orgId) {\n throw new Error(\"asset org mismatch\");\n }\n const [row] = await this.db\n .insert(assets)\n .values({\n id: asset.id,\n orgId: asset.orgId,\n key: asset.key,\n kind: asset.kind,\n source: asset.source,\n filename: asset.filename,\n contentType: asset.contentType,\n size: asset.size,\n status: asset.status,\n uploadedBy: asset.uploadedBy,\n contentId: asset.contentId,\n createdAt: new Date(asset.createdAt),\n })\n .returning();\n if (!row) {\n throw new Error(\"failed to insert asset\");\n }\n return toAsset(row);\n }\n\n async list(orgId: string, query: AssetsQuery): Promise<Page<Asset>> {\n const filters: (SQL | undefined)[] = [eq(assets.orgId, orgId)];\n if (query.kind) {\n filters.push(eq(assets.kind, query.kind));\n }\n if (query.source) {\n filters.push(eq(assets.source, query.source));\n }\n if (query.contentId) {\n filters.push(eq(assets.contentId, query.contentId));\n }\n const q = query.search?.trim();\n if (q) {\n filters.push(ilike(assets.filename, `%${q}%`));\n }\n const where = and(...filters);\n\n const orderBy = (() => {\n if (!query.sort) {\n return desc(assets.createdAt);\n }\n const descending = query.sort.startsWith(\"-\");\n const field = descending ? query.sort.slice(1) : query.sort;\n const col = SORT_COLUMNS[field as keyof typeof SORT_COLUMNS] ?? assets.createdAt;\n return descending ? desc(col) : asc(col);\n })();\n\n const offset = (query.page - 1) * query.pageSize;\n const rows = await this.db\n .select()\n .from(assets)\n .where(where)\n .orderBy(orderBy)\n .limit(query.pageSize)\n .offset(offset);\n\n const counted = await this.db\n .select({ value: sql<number>`count(*)::int` })\n .from(assets)\n .where(where);\n\n return {\n rows: rows.map(toAsset),\n total: counted[0]?.value ?? 0,\n page: query.page,\n pageSize: query.pageSize,\n };\n }\n\n async findById(orgId: string, id: string): Promise<Asset | null> {\n const [row] = await this.db\n .select()\n .from(assets)\n .where(and(eq(assets.orgId, orgId), eq(assets.id, id)))\n .limit(1);\n return row ? toAsset(row) : null;\n }\n\n async findByIds(orgId: string, ids: string[]): Promise<Asset[]> {\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.db\n .select()\n .from(assets)\n .where(and(eq(assets.orgId, orgId), inArray(assets.id, ids)));\n return rows.map(toAsset);\n }\n\n async markReady(\n orgId: string,\n id: string,\n patch: Pick<Asset, \"key\" | \"kind\" | \"size\" | \"contentType\">,\n ): Promise<Asset | null> {\n const [row] = await this.db\n .update(assets)\n .set({\n key: patch.key,\n kind: patch.kind,\n size: patch.size,\n contentType: patch.contentType,\n status: \"ready\",\n })\n .where(and(eq(assets.orgId, orgId), eq(assets.id, id), eq(assets.status, \"pending\")))\n .returning();\n return row ? toAsset(row) : null;\n }\n\n async delete(orgId: string, id: string): Promise<boolean> {\n try {\n const deleted = await this.db\n .delete(assets)\n .where(and(eq(assets.orgId, orgId), eq(assets.id, id)))\n .returning({ id: assets.id });\n return deleted.length > 0;\n } catch (err) {\n if (pgErrorFields(err)?.code === \"23503\") {\n throw Object.assign(new ConflictError(\"This file is still in use\"), { cause: err });\n }\n throw err;\n }\n }\n\n async listUnreferencedMemberUploads(orgId: string, before: Date): Promise<Asset[]> {\n const rows = await this.db\n .select()\n .from(assets)\n .where(unreferencedMemberUploads(orgId, before))\n .orderBy(asc(assets.createdAt));\n return rows.map(toAsset);\n }\n\n async heldKeys(orgId: string, keys: string[]): Promise<string[]> {\n if (keys.length === 0) {\n return [];\n }\n const rows = await this.db\n .select({ key: assets.key })\n .from(assets)\n .where(and(eq(assets.orgId, orgId), inArray(assets.key, keys)));\n return rows.map((r) => r.key);\n }\n\n async contentTypeOf(orgId: string, contentId: string): Promise<ContentType | null> {\n const [row] = await this.db\n .select({ type: contentItems.type })\n .from(contentItems)\n .where(and(eq(contentItems.orgId, orgId), eq(contentItems.id, contentId)))\n .limit(1);\n return row?.type ?? null;\n }\n}\ntranslateDbErrors(DrizzleAssetsRepository);\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  import { automationRuns, automations } from "../schema/automations.js";
2
2
  import "../schema/index.js";
3
3
  import { translateDbErrors } from "./pg-errors.js";
4
- import { and, arrayContains, asc, desc, eq, gte, ne, sql } from "drizzle-orm";
5
4
  import { noopLogger } from "@headless-lms/core/shared/logger";
5
+ import { and, arrayContains, asc, desc, eq, gte, ne, sql } from "drizzle-orm";
6
6
  //#region src/repositories/automations.ts
7
7
  const automationSelection = {
8
8
  orgId: automations.orgId,
@@ -1,6 +1,6 @@
1
1
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
2
2
  import { Chat, ChatUser, Logger, Message, MessageFlag, MessageFlagResolution, Page, ReactionEmoji } from "@headless-lms/core/types";
3
- import { ChatInboxRow, ChatRepository, ChatUnread, FeedQuery, MessageReactions, PageQuery } from "@headless-lms/core/discussion";
3
+ import { ChatInboxRow, ChatListQuery, ChatMemberStates, ChatRepository, ChatUnread, ChatUnreadSummary, FeedQuery, MessageReactions, ReactionChange } from "@headless-lms/core/discussion";
4
4
  //#region src/repositories/chats.d.ts
5
5
  declare class DrizzleChatRepository implements ChatRepository {
6
6
  private readonly db;
@@ -11,11 +11,16 @@ declare class DrizzleChatRepository implements ChatRepository {
11
11
  insertChat(orgId: string, chat: Chat): Promise<Chat>;
12
12
  updateChat(orgId: string, id: string, patch: Partial<Pick<Chat, "status" | "lockedAt" | "lockedBy">>): Promise<Chat | null>;
13
13
  findChat(orgId: string, id: string): Promise<Chat | null>;
14
- listForUser(orgId: string, communityId: string, orgUserId: string, query: PageQuery): Promise<Page<ChatInboxRow>>;
14
+ listForUser(orgId: string, communityId: string, orgUserId: string, query: ChatListQuery): Promise<Page<ChatInboxRow>>;
15
+ inboxRow(orgId: string, chatId: string, orgUserId: string): Promise<ChatInboxRow | null>;
16
+ activeParticipants(orgId: string, chatId: string): Promise<string[]>;
17
+ unreadSummary(orgId: string, communityId: string, orgUserId: string, at: Date): Promise<ChatUnreadSummary>;
18
+ memberStates(orgId: string, communityId: string, orgUserIds: string[], at: Date): Promise<ChatMemberStates[]>;
19
+ findByClientId(orgId: string, chatId: string, orgUserId: string, clientId: string): Promise<Message | null>;
15
20
  participants(orgId: string, chatId: string): Promise<ChatUser[]>;
16
21
  join(orgId: string, user: ChatUser): Promise<void>;
17
22
  markLeft(orgId: string, chatId: string, orgUserId: string, leftAt: Date): Promise<void>;
18
- setRead(orgId: string, chatId: string, orgUserId: string, messageId: string): Promise<void>;
23
+ setRead(orgId: string, chatId: string, orgUserId: string, messageId: string): Promise<boolean>;
19
24
  unreadFor(orgId: string, chatId: string, orgUserId: string): Promise<ChatUnread>;
20
25
  insertMessage(orgId: string, message: Message): Promise<Message>;
21
26
  updateMessage(orgId: string, id: string, patch: Partial<Pick<Message, "body" | "editedAt" | "deletedAt">>): Promise<Message | null>;
@@ -23,7 +28,7 @@ declare class DrizzleChatRepository implements ChatRepository {
23
28
  listMessages(orgId: string, chatId: string, query: Pick<FeedQuery, "page" | "pageSize">): Promise<Page<Message>>;
24
29
  attach(orgId: string, communityId: string, messageId: string, assetIds: string[]): Promise<void>;
25
30
  attachmentsOf(orgId: string, messageIds: string[]): Promise<Record<string, string[]>>;
26
- setReaction(orgId: string, communityId: string, messageId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<boolean>;
31
+ setReaction(orgId: string, communityId: string, messageId: string, orgUserId: string, emoji: ReactionEmoji | null): Promise<ReactionChange>;
27
32
  reactionsOf(orgId: string, messageIds: string[], viewerId: string): Promise<Record<string, MessageReactions>>;
28
33
  block(orgId: string, communityId: string, blockerOrgUserId: string, blockedOrgUserId: string): Promise<void>;
29
34
  unblock(orgId: string, communityId: string, blockerOrgUserId: string, blockedOrgUserId: string): Promise<void>;
@@ -48,6 +53,10 @@ declare class DrizzleChatRepository implements ChatRepository {
48
53
  resolvedBy: string;
49
54
  resolution: MessageFlagResolution;
50
55
  }): Promise<void>;
56
+ private requestTo;
57
+ private unreadCount;
58
+ private inbox;
59
+ private withParticipants;
51
60
  private touch;
52
61
  }
53
62
  //#endregion
@@ -1,9 +1,9 @@
1
- import { blockedUsers, chatUsers, chats, messageAttachments, messageFlags, messageReactions, messages } from "../schema/discussion.js";
1
+ import { blockedUsers, chatUsers, chats, messageAttachments, messageFlags, messageReactions, messages, subscriptions } from "../schema/discussion.js";
2
2
  import "../schema/index.js";
3
3
  import { translateDbErrors } from "./pg-errors.js";
4
4
  import { insertedRow } from "./inserted-row.js";
5
- import { and, asc, desc, eq, gt, inArray, isNull, ne, or, sql } from "drizzle-orm";
6
5
  import { noopLogger } from "@headless-lms/core/shared/logger";
6
+ import { and, asc, desc, eq, gt, inArray, isNull, ne, or, sql } from "drizzle-orm";
7
7
  //#region src/repositories/chats.ts
8
8
  function toChat(row) {
9
9
  return {
@@ -41,6 +41,7 @@ function toMessage(row) {
41
41
  orgUserId: row.orgUserId,
42
42
  body: row.body,
43
43
  replyToId: row.replyToId,
44
+ clientId: row.clientId,
44
45
  editedAt: row.editedAt,
45
46
  deletedAt: row.deletedAt,
46
47
  createdAt: row.createdAt,
@@ -126,75 +127,81 @@ var DrizzleChatRepository = class {
126
127
  return row ? toChat(row) : null;
127
128
  }
128
129
  async listForUser(orgId, communityId, orgUserId, query) {
129
- const unread = sql`cast((
130
- select count(*)
131
- from messages m
132
- where m.org_id = ${chats.orgId}
133
- and m.chat_id = ${chats.id}
134
- and m.org_user_id <> ${orgUserId}
135
- and m.deleted_at is null
136
- and (
137
- ${chatUsers.lastReadMessageId} is null
138
- or m.created_at > coalesce((
139
- select lr.created_at
140
- from messages lr
141
- where lr.org_id = ${chats.orgId}
142
- and lr.id = ${chatUsers.lastReadMessageId}
143
- ), '-infinity'::timestamptz)
144
- )
145
- ) as int)`;
146
- const latest = this.db.select({
147
- id: messages.id,
148
- orgUserId: messages.orgUserId,
149
- body: messages.body,
150
- createdAt: messages.createdAt,
151
- deletedAt: messages.deletedAt
152
- }).from(messages).where(and(eq(messages.orgId, chats.orgId), eq(messages.chatId, chats.id))).orderBy(desc(messages.createdAt), desc(messages.id)).limit(1).as("last_message");
153
- const mine = and(eq(chatUsers.orgId, orgId), eq(chatUsers.communityId, communityId), eq(chatUsers.orgUserId, orgUserId), isNull(chatUsers.leftAt));
154
- const rows = await this.db.select({
155
- chat: chats,
156
- lastReadMessageId: chatUsers.lastReadMessageId,
157
- unread,
158
- lastId: latest.id,
159
- lastOrgUserId: latest.orgUserId,
160
- lastBody: latest.body,
161
- lastCreatedAt: latest.createdAt,
162
- lastDeletedAt: latest.deletedAt
163
- }).from(chatUsers).innerJoin(chats, and(eq(chats.orgId, chatUsers.orgId), eq(chats.id, chatUsers.chatId))).leftJoinLateral(latest, sql`true`).where(mine).orderBy(desc(chats.updatedAt), desc(chats.id)).limit(query.pageSize).offset((query.page - 1) * query.pageSize);
164
- const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(chatUsers).where(mine);
130
+ const request = this.requestTo(orgUserId);
131
+ const mine = and(eq(chatUsers.orgId, orgId), eq(chatUsers.communityId, communityId), eq(chatUsers.orgUserId, orgUserId), isNull(chatUsers.leftAt), query.requests === void 0 ? void 0 : query.requests ? request : sql`not ${request}`);
132
+ const rows = await this.inbox(orgUserId, mine, query);
133
+ const [{ total } = { total: 0 }] = await this.db.select({ total: sql`cast(count(*) as int)` }).from(chatUsers).innerJoin(chats, and(eq(chats.orgId, chatUsers.orgId), eq(chats.id, chatUsers.chatId))).where(mine);
165
134
  const page = {
166
135
  total,
167
136
  page: query.page,
168
137
  pageSize: query.pageSize
169
138
  };
170
- if (rows.length === 0) return {
171
- rows: [],
172
- ...page
173
- };
174
- const people = await this.db.select().from(chatUsers).where(and(eq(chatUsers.orgId, orgId), inArray(chatUsers.chatId, rows.map((row) => row.chat.id)), isNull(chatUsers.leftAt)));
175
- const byChat = /* @__PURE__ */ new Map();
176
- for (const person of people) {
177
- const list = byChat.get(person.chatId) ?? [];
178
- list.push(person.orgUserId);
179
- byChat.set(person.chatId, list);
180
- }
181
139
  return {
182
- rows: rows.map((row) => ({
183
- chat: toChat(row.chat),
184
- participantIds: (byChat.get(row.chat.id) ?? []).sort(),
185
- lastReadMessageId: row.lastReadMessageId,
186
- unread: row.unread,
187
- lastMessage: row.lastId !== null && row.lastOrgUserId !== null && row.lastCreatedAt !== null ? {
188
- id: row.lastId,
189
- orgUserId: row.lastOrgUserId,
190
- body: row.lastBody ?? "",
191
- createdAt: row.lastCreatedAt,
192
- deletedAt: row.lastDeletedAt
193
- } : null
194
- })),
140
+ rows: await this.withParticipants(orgId, rows),
195
141
  ...page
196
142
  };
197
143
  }
144
+ async inboxRow(orgId, chatId, orgUserId) {
145
+ const rows = await this.inbox(orgUserId, and(eq(chatUsers.orgId, orgId), eq(chatUsers.chatId, chatId), eq(chatUsers.orgUserId, orgUserId), isNull(chatUsers.leftAt)));
146
+ const [row] = await this.withParticipants(orgId, rows);
147
+ return row ?? null;
148
+ }
149
+ async activeParticipants(orgId, chatId) {
150
+ return (await this.db.select({ orgUserId: chatUsers.orgUserId }).from(chatUsers).where(and(eq(chatUsers.orgId, orgId), eq(chatUsers.chatId, chatId), isNull(chatUsers.leftAt))).orderBy(asc(chatUsers.orgUserId))).map((row) => row.orgUserId);
151
+ }
152
+ async unreadSummary(orgId, communityId, orgUserId, at) {
153
+ const [states] = await this.memberStates(orgId, communityId, [orgUserId], at);
154
+ return states?.unread ?? {
155
+ unread: 0,
156
+ requests: 0
157
+ };
158
+ }
159
+ async memberStates(orgId, communityId, orgUserIds, at) {
160
+ const ids = [...new Set(orgUserIds)];
161
+ if (ids.length === 0) return [];
162
+ const rows = await this.db.select({
163
+ orgUserId: chatUsers.orgUserId,
164
+ chatId: chatUsers.chatId,
165
+ lastReadMessageId: chatUsers.lastReadMessageId,
166
+ kind: chats.kind,
167
+ status: chats.status,
168
+ createdBy: chats.createdBy,
169
+ state: subscriptions.state,
170
+ mutedUntil: subscriptions.mutedUntil,
171
+ unread: this.unreadCount(chatUsers.orgUserId)
172
+ }).from(chatUsers).innerJoin(chats, and(eq(chats.orgId, chatUsers.orgId), eq(chats.id, chatUsers.chatId))).leftJoin(subscriptions, and(eq(subscriptions.orgId, chatUsers.orgId), eq(subscriptions.orgUserId, chatUsers.orgUserId), eq(subscriptions.itemType, "chat"), eq(subscriptions.itemId, chatUsers.chatId))).where(and(eq(chatUsers.orgId, orgId), eq(chatUsers.communityId, communityId), inArray(chatUsers.orgUserId, ids), isNull(chatUsers.leftAt))).orderBy(asc(chatUsers.orgUserId), asc(chatUsers.chatId));
173
+ const byMember = new Map(ids.map((id) => [id, {
174
+ orgUserId: id,
175
+ unread: {
176
+ unread: 0,
177
+ requests: 0
178
+ },
179
+ chats: []
180
+ }]));
181
+ for (const row of rows) {
182
+ const entry = byMember.get(row.orgUserId);
183
+ const lapsed = row.state === "muted" && row.mutedUntil !== null && row.mutedUntil.getTime() <= at.getTime();
184
+ const subscription = row.state !== null && !lapsed ? {
185
+ state: row.state,
186
+ mutedUntil: row.mutedUntil
187
+ } : null;
188
+ if (row.kind === "direct" && row.status === "pending" && row.createdBy !== row.orgUserId) {
189
+ entry.unread.requests += 1;
190
+ entry.unread.unread += 1;
191
+ } else if (subscription?.state !== "muted") entry.unread.unread += row.unread;
192
+ entry.chats.push({
193
+ chatId: row.chatId,
194
+ unread: row.unread,
195
+ lastReadMessageId: row.lastReadMessageId,
196
+ subscription
197
+ });
198
+ }
199
+ return [...byMember.values()];
200
+ }
201
+ async findByClientId(orgId, chatId, orgUserId, clientId) {
202
+ const [row] = await this.db.select().from(messages).where(and(eq(messages.orgId, orgId), eq(messages.chatId, chatId), eq(messages.orgUserId, orgUserId), eq(messages.clientId, clientId))).limit(1);
203
+ return row ? toMessage(row) : null;
204
+ }
198
205
  async participants(orgId, chatId) {
199
206
  return (await this.db.select().from(chatUsers).where(and(eq(chatUsers.orgId, orgId), eq(chatUsers.chatId, chatId))).orderBy(asc(chatUsers.orgUserId))).map(toChatUser);
200
207
  }
@@ -230,10 +237,25 @@ var DrizzleChatRepository = class {
230
237
  await this.touch(orgId, chatId);
231
238
  }
232
239
  async setRead(orgId, chatId, orgUserId, messageId) {
233
- await this.db.update(chatUsers).set({
240
+ return (await this.db.update(chatUsers).set({
234
241
  lastReadMessageId: messageId,
235
242
  updatedAt: /* @__PURE__ */ new Date()
236
- }).where(and(eq(chatUsers.orgId, orgId), eq(chatUsers.chatId, chatId), eq(chatUsers.orgUserId, orgUserId)));
243
+ }).where(and(eq(chatUsers.orgId, orgId), eq(chatUsers.chatId, chatId), eq(chatUsers.orgUserId, orgUserId), sql`(
244
+ ${chatUsers.lastReadMessageId} is null
245
+ or not exists (
246
+ select 1 from messages cur
247
+ where cur.org_id = ${chatUsers.orgId} and cur.id = ${chatUsers.lastReadMessageId}
248
+ )
249
+ or exists (
250
+ select 1
251
+ from messages target
252
+ join messages cur on cur.org_id = target.org_id
253
+ where target.org_id = ${chatUsers.orgId}
254
+ and target.id = ${messageId}
255
+ and cur.id = ${chatUsers.lastReadMessageId}
256
+ and (target.created_at, target.id) > (cur.created_at, cur.id)
257
+ )
258
+ )`)).returning({ orgUserId: chatUsers.orgUserId })).length > 0;
237
259
  }
238
260
  async unreadFor(orgId, chatId, orgUserId) {
239
261
  const readUpTo = sql`coalesce((
@@ -271,6 +293,7 @@ var DrizzleChatRepository = class {
271
293
  orgUserId: message.orgUserId,
272
294
  body: message.body,
273
295
  replyToId: message.replyToId,
296
+ clientId: message.clientId,
274
297
  editedAt: message.editedAt,
275
298
  deletedAt: message.deletedAt,
276
299
  createdAt: message.createdAt,
@@ -328,10 +351,10 @@ var DrizzleChatRepository = class {
328
351
  return byMessage;
329
352
  }
330
353
  async setReaction(orgId, communityId, messageId, orgUserId, emoji) {
331
- if (emoji === null) {
332
- await this.db.delete(messageReactions).where(and(eq(messageReactions.orgId, orgId), eq(messageReactions.messageId, messageId), eq(messageReactions.orgUserId, orgUserId)));
333
- return false;
334
- }
354
+ if (emoji === null) return {
355
+ inserted: false,
356
+ changed: (await this.db.delete(messageReactions).where(and(eq(messageReactions.orgId, orgId), eq(messageReactions.messageId, messageId), eq(messageReactions.orgUserId, orgUserId))).returning({ orgUserId: messageReactions.orgUserId })).length > 0
357
+ };
335
358
  const [row] = await this.db.insert(messageReactions).values({
336
359
  orgId,
337
360
  communityId,
@@ -347,9 +370,13 @@ var DrizzleChatRepository = class {
347
370
  set: {
348
371
  emoji,
349
372
  updatedAt: /* @__PURE__ */ new Date()
350
- }
373
+ },
374
+ setWhere: sql`${messageReactions.emoji} is distinct from excluded.emoji`
351
375
  }).returning({ inserted: insertedRow });
352
- return row?.inserted === true;
376
+ return {
377
+ inserted: row?.inserted === true,
378
+ changed: row !== void 0
379
+ };
353
380
  }
354
381
  async reactionsOf(orgId, messageIds, viewerId) {
355
382
  if (messageIds.length === 0) return {};
@@ -458,6 +485,73 @@ var DrizzleChatRepository = class {
458
485
  updatedAt: /* @__PURE__ */ new Date()
459
486
  }).where(and(eq(messageFlags.orgId, orgId), eq(messageFlags.messageId, messageId), isNull(messageFlags.resolvedAt)));
460
487
  }
488
+ requestTo(orgUserId) {
489
+ return sql`(${chats.kind} = 'direct' and ${chats.status} = 'pending' and ${chats.createdBy} is distinct from ${orgUserId})`;
490
+ }
491
+ unreadCount(orgUserId) {
492
+ return sql`cast((
493
+ select count(*)
494
+ from messages m
495
+ where m.org_id = ${chatUsers.orgId}
496
+ and m.chat_id = ${chatUsers.chatId}
497
+ and m.org_user_id <> ${orgUserId}
498
+ and m.deleted_at is null
499
+ and (
500
+ ${chatUsers.lastReadMessageId} is null
501
+ or m.created_at > coalesce((
502
+ select lr.created_at
503
+ from messages lr
504
+ where lr.org_id = ${chatUsers.orgId}
505
+ and lr.id = ${chatUsers.lastReadMessageId}
506
+ ), '-infinity'::timestamptz)
507
+ )
508
+ ) as int)`;
509
+ }
510
+ async inbox(orgUserId, where, query) {
511
+ const latest = this.db.select({
512
+ id: messages.id,
513
+ orgUserId: messages.orgUserId,
514
+ body: messages.body,
515
+ createdAt: messages.createdAt,
516
+ deletedAt: messages.deletedAt
517
+ }).from(messages).where(and(eq(messages.orgId, chats.orgId), eq(messages.chatId, chats.id))).orderBy(desc(messages.createdAt), desc(messages.id)).limit(1).as("last_message");
518
+ const select = this.db.select({
519
+ chat: chats,
520
+ lastReadMessageId: chatUsers.lastReadMessageId,
521
+ unread: this.unreadCount(orgUserId),
522
+ lastId: latest.id,
523
+ lastOrgUserId: latest.orgUserId,
524
+ lastBody: latest.body,
525
+ lastCreatedAt: latest.createdAt,
526
+ lastDeletedAt: latest.deletedAt
527
+ }).from(chatUsers).innerJoin(chats, and(eq(chats.orgId, chatUsers.orgId), eq(chats.id, chatUsers.chatId))).leftJoinLateral(latest, sql`true`).where(where).orderBy(desc(chats.updatedAt), desc(chats.id));
528
+ return (query ? await select.limit(query.pageSize).offset((query.page - 1) * query.pageSize) : await select.limit(1)).map((row) => ({
529
+ chat: toChat(row.chat),
530
+ lastReadMessageId: row.lastReadMessageId,
531
+ unread: row.unread,
532
+ lastMessage: row.lastId !== null && row.lastOrgUserId !== null && row.lastCreatedAt !== null ? {
533
+ id: row.lastId,
534
+ orgUserId: row.lastOrgUserId,
535
+ body: row.lastBody ?? "",
536
+ createdAt: row.lastCreatedAt,
537
+ deletedAt: row.lastDeletedAt
538
+ } : null
539
+ }));
540
+ }
541
+ async withParticipants(orgId, rows) {
542
+ if (rows.length === 0) return [];
543
+ const people = await this.db.select().from(chatUsers).where(and(eq(chatUsers.orgId, orgId), inArray(chatUsers.chatId, rows.map((row) => row.chat.id)), isNull(chatUsers.leftAt)));
544
+ const byChat = /* @__PURE__ */ new Map();
545
+ for (const person of people) {
546
+ const list = byChat.get(person.chatId) ?? [];
547
+ list.push(person.orgUserId);
548
+ byChat.set(person.chatId, list);
549
+ }
550
+ return rows.map((row) => ({
551
+ ...row,
552
+ participantIds: (byChat.get(row.chat.id) ?? []).sort()
553
+ }));
554
+ }
461
555
  async touch(orgId, chatId) {
462
556
  await this.db.update(chats).set({ updatedAt: /* @__PURE__ */ new Date() }).where(and(eq(chats.orgId, orgId), eq(chats.id, chatId)));
463
557
  }