@igstack/app-catalog-backend-core 6.10.2 → 6.11.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 (104) hide show
  1. package/dist/db/syncAppCatalog.mjs +1 -1
  2. package/dist/generated/prisma/client.d.mts +1 -1
  3. package/dist/generated/prisma/client.mjs.map +1 -1
  4. package/dist/generated/prisma/commonInputTypes.d.mts +33 -3
  5. package/dist/generated/prisma/commonInputTypes.d.mts.map +1 -1
  6. package/dist/generated/prisma/enums.d.mts +6 -1
  7. package/dist/generated/prisma/enums.d.mts.map +1 -1
  8. package/dist/generated/prisma/internal/class.d.mts +84 -0
  9. package/dist/generated/prisma/internal/class.d.mts.map +1 -1
  10. package/dist/generated/prisma/internal/class.mjs +4 -4
  11. package/dist/generated/prisma/internal/class.mjs.map +1 -1
  12. package/dist/generated/prisma/internal/prismaNamespace.d.mts +617 -4
  13. package/dist/generated/prisma/internal/prismaNamespace.d.mts.map +1 -1
  14. package/dist/generated/prisma/models/DbCommunityTool.d.mts +2852 -0
  15. package/dist/generated/prisma/models/DbCommunityTool.d.mts.map +1 -0
  16. package/dist/generated/prisma/models/DbCommunityToolLike.d.mts +1102 -0
  17. package/dist/generated/prisma/models/DbCommunityToolLike.d.mts.map +1 -0
  18. package/dist/generated/prisma/models/DbCommunityToolNamedOwner.d.mts +1034 -0
  19. package/dist/generated/prisma/models/DbCommunityToolNamedOwner.d.mts.map +1 -0
  20. package/dist/generated/prisma/models/DbCommunityToolOwner.d.mts +1034 -0
  21. package/dist/generated/prisma/models/DbCommunityToolOwner.d.mts.map +1 -0
  22. package/dist/generated/prisma/models/DbCommunityToolPin.d.mts +1110 -0
  23. package/dist/generated/prisma/models/DbCommunityToolPin.d.mts.map +1 -0
  24. package/dist/generated/prisma/models/DbCommunityToolTag.d.mts +1073 -0
  25. package/dist/generated/prisma/models/DbCommunityToolTag.d.mts.map +1 -0
  26. package/dist/generated/prisma/models/DbCommunityToolView.d.mts +1149 -0
  27. package/dist/generated/prisma/models/DbCommunityToolView.d.mts.map +1 -0
  28. package/dist/generated/prisma/models/DbFeedback.d.mts +361 -2
  29. package/dist/generated/prisma/models/DbFeedback.d.mts.map +1 -1
  30. package/dist/generated/prisma/models.d.mts +9 -2
  31. package/dist/index.d.mts +5 -2
  32. package/dist/index.mjs +3 -1
  33. package/dist/middleware/createAcMiddleware.d.mts.map +1 -1
  34. package/dist/middleware/createAcMiddleware.mjs +15 -1
  35. package/dist/middleware/createAcMiddleware.mjs.map +1 -1
  36. package/dist/middleware/database.d.mts +1 -1
  37. package/dist/middleware/types.d.mts +20 -1
  38. package/dist/middleware/types.d.mts.map +1 -1
  39. package/dist/modules/communityTools/capture.d.mts +27 -0
  40. package/dist/modules/communityTools/capture.d.mts.map +1 -0
  41. package/dist/modules/communityTools/capture.mjs +192 -0
  42. package/dist/modules/communityTools/capture.mjs.map +1 -0
  43. package/dist/modules/communityTools/healthJob.mjs +132 -0
  44. package/dist/modules/communityTools/healthJob.mjs.map +1 -0
  45. package/dist/modules/communityTools/router.d.mts +150 -0
  46. package/dist/modules/communityTools/router.d.mts.map +1 -0
  47. package/dist/modules/communityTools/router.mjs +91 -0
  48. package/dist/modules/communityTools/router.mjs.map +1 -0
  49. package/dist/modules/communityTools/service.mjs +252 -0
  50. package/dist/modules/communityTools/service.mjs.map +1 -0
  51. package/dist/modules/communityTools/toView.mjs +81 -0
  52. package/dist/modules/communityTools/toView.mjs.map +1 -0
  53. package/dist/modules/communityTools/writeService.d.mts +55 -0
  54. package/dist/modules/communityTools/writeService.d.mts.map +1 -0
  55. package/dist/modules/communityTools/writeService.mjs +280 -0
  56. package/dist/modules/communityTools/writeService.mjs.map +1 -0
  57. package/dist/modules/feedback/feedbackRouter.d.mts +3 -0
  58. package/dist/modules/feedback/feedbackRouter.mjs +6 -2
  59. package/dist/modules/feedback/feedbackRouter.mjs.map +1 -1
  60. package/dist/modules/feedback/service.d.mts +3 -0
  61. package/dist/modules/feedback/service.d.mts.map +1 -1
  62. package/dist/modules/feedback/service.mjs +50 -17
  63. package/dist/modules/feedback/service.mjs.map +1 -1
  64. package/dist/server/controller.d.mts +143 -0
  65. package/dist/server/controller.d.mts.map +1 -1
  66. package/dist/server/controller.mjs +2 -0
  67. package/dist/server/controller.mjs.map +1 -1
  68. package/dist/server/trpcSetup.mjs +1 -1
  69. package/package.json +12 -5
  70. package/prisma/schema.prisma +161 -0
  71. package/src/__tests__/communityTools.test.ts +781 -0
  72. package/src/__tests__/communityToolsCapture.test.ts +319 -0
  73. package/src/__tests__/communityToolsHealth.test.ts +154 -0
  74. package/src/__tests__/communityToolsRouter.test.ts +58 -0
  75. package/src/__tests__/communityToolsWrite.test.ts +561 -0
  76. package/src/__tests__/feedbackService.test.ts +160 -0
  77. package/src/generated/prisma/browser.ts +35 -0
  78. package/src/generated/prisma/client.ts +35 -0
  79. package/src/generated/prisma/commonInputTypes.ts +34 -0
  80. package/src/generated/prisma/enums.ts +8 -0
  81. package/src/generated/prisma/internal/class.ts +74 -4
  82. package/src/generated/prisma/internal/prismaNamespace.ts +635 -2
  83. package/src/generated/prisma/internal/prismaNamespaceBrowser.ts +95 -1
  84. package/src/generated/prisma/models/DbCommunityTool.ts +3135 -0
  85. package/src/generated/prisma/models/DbCommunityToolLike.ts +1313 -0
  86. package/src/generated/prisma/models/DbCommunityToolNamedOwner.ts +1243 -0
  87. package/src/generated/prisma/models/DbCommunityToolOwner.ts +1243 -0
  88. package/src/generated/prisma/models/DbCommunityToolPin.ts +1325 -0
  89. package/src/generated/prisma/models/DbCommunityToolTag.ts +1283 -0
  90. package/src/generated/prisma/models/DbCommunityToolView.ts +1361 -0
  91. package/src/generated/prisma/models/DbFeedback.ts +379 -1
  92. package/src/generated/prisma/models.ts +7 -0
  93. package/src/index.ts +16 -0
  94. package/src/middleware/createAcMiddleware.ts +20 -0
  95. package/src/middleware/types.ts +21 -0
  96. package/src/modules/communityTools/capture.ts +332 -0
  97. package/src/modules/communityTools/healthJob.ts +191 -0
  98. package/src/modules/communityTools/router.ts +172 -0
  99. package/src/modules/communityTools/service.ts +380 -0
  100. package/src/modules/communityTools/toView.ts +112 -0
  101. package/src/modules/communityTools/writeService.ts +408 -0
  102. package/src/modules/feedback/feedbackRouter.ts +20 -6
  103. package/src/modules/feedback/service.ts +68 -12
  104. package/src/server/controller.ts +8 -1
@@ -0,0 +1,252 @@
1
+ import { getDbClient } from "../../db/client.mjs";
2
+ import { LIKES, toView } from "./toView.mjs";
3
+ import { TRPCError } from "@trpc/server";
4
+
5
+ //#region src/modules/communityTools/service.ts
6
+ async function listTools(user, isAdmin, now, capture) {
7
+ const db = getDbClient();
8
+ const tools = await db.dbCommunityTool.findMany({ include: {
9
+ owners: true,
10
+ namedOwners: true,
11
+ likes: LIKES,
12
+ tags: true
13
+ } });
14
+ const toolIds = tools.map((t) => t.id);
15
+ const ownerSlugs = tools.flatMap((t) => t.owners.map((o) => o.personSlug));
16
+ const uniqueOwnerSlugs = Array.from(new Set(ownerSlugs));
17
+ const persons = await db.dbPerson.findMany({ where: { slug: { in: uniqueOwnerSlugs } } });
18
+ const personMap = new Map(persons.map((p) => [p.slug, p]));
19
+ const today = new Date(now);
20
+ today.setUTCHours(0, 0, 0, 0);
21
+ const thirtyDaysAgo = new Date(today);
22
+ thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 29);
23
+ const viewCounts = await Promise.all(toolIds.map(async (toolId) => {
24
+ const [views30d, viewsAll] = await Promise.all([db.dbCommunityToolView.count({ where: {
25
+ toolId,
26
+ day: { gte: thirtyDaysAgo }
27
+ } }), db.dbCommunityToolView.count({ where: { toolId } })]);
28
+ return {
29
+ toolId,
30
+ views30d,
31
+ viewsAll
32
+ };
33
+ }));
34
+ const viewCountMap = new Map(viewCounts.map((v) => [v.toolId, {
35
+ views30d: v.views30d,
36
+ viewsAll: v.viewsAll
37
+ }]));
38
+ return Promise.all(tools.map((tool) => toView(tool, personMap, viewCountMap.get(tool.id), user, isAdmin, now, capture)));
39
+ }
40
+ /** How many live tools carry each official app's tag, by app slug. */
41
+ async function countByApp() {
42
+ const rows = await getDbClient().dbCommunityToolTag.groupBy({
43
+ by: ["value"],
44
+ where: {
45
+ kind: "app",
46
+ tool: { archivedAt: null }
47
+ },
48
+ _count: { _all: true }
49
+ });
50
+ return Object.fromEntries(rows.map((r) => [r.value, r._count._all]));
51
+ }
52
+ async function getTool(slug, user, isAdmin, now, capture) {
53
+ const db = getDbClient();
54
+ const tool = await db.dbCommunityTool.findUnique({
55
+ where: { slug },
56
+ include: {
57
+ owners: true,
58
+ namedOwners: true,
59
+ likes: LIKES,
60
+ tags: true
61
+ }
62
+ });
63
+ if (!tool) return null;
64
+ const ownerSlugs = tool.owners.map((o) => o.personSlug);
65
+ const persons = await db.dbPerson.findMany({ where: { slug: { in: ownerSlugs } } });
66
+ const personMap = new Map(persons.map((p) => [p.slug, p]));
67
+ const today = new Date(now);
68
+ today.setUTCHours(0, 0, 0, 0);
69
+ const thirtyDaysAgo = new Date(today);
70
+ thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 29);
71
+ const [views30d, viewsAll] = await Promise.all([db.dbCommunityToolView.count({ where: {
72
+ toolId: tool.id,
73
+ day: { gte: thirtyDaysAgo }
74
+ } }), db.dbCommunityToolView.count({ where: { toolId: tool.id } })]);
75
+ return toView(tool, personMap, {
76
+ views30d,
77
+ viewsAll
78
+ }, user, isAdmin, now, capture);
79
+ }
80
+ async function recordView(slug, actor, user, now) {
81
+ const db = getDbClient();
82
+ const tool = await db.dbCommunityTool.findUnique({ where: { slug } });
83
+ if (!tool) throw new Error("Tool not found");
84
+ const day = new Date(now);
85
+ day.setUTCHours(0, 0, 0, 0);
86
+ await db.dbCommunityToolView.upsert({
87
+ where: { toolId_actorHash_day: {
88
+ toolId: tool.id,
89
+ actorHash: actor.hash,
90
+ day
91
+ } },
92
+ create: {
93
+ toolId: tool.id,
94
+ actorHash: actor.hash,
95
+ userId: user?.id ?? null,
96
+ day,
97
+ at: now
98
+ },
99
+ update: {
100
+ at: now,
101
+ userId: user?.id ?? null
102
+ }
103
+ });
104
+ const thirtyDaysAgo = new Date(day);
105
+ thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 29);
106
+ const [views30d, viewsAll] = await Promise.all([db.dbCommunityToolView.count({ where: {
107
+ toolId: tool.id,
108
+ day: { gte: thirtyDaysAgo }
109
+ } }), db.dbCommunityToolView.count({ where: { toolId: tool.id } })]);
110
+ return {
111
+ views30d,
112
+ viewsAll
113
+ };
114
+ }
115
+ async function listPins(userId, user, isAdmin, now, capture) {
116
+ const db = getDbClient();
117
+ const pins = await db.dbCommunityToolPin.findMany({
118
+ where: { userId },
119
+ orderBy: { position: "asc" },
120
+ include: { tool: { include: {
121
+ owners: true,
122
+ namedOwners: true,
123
+ likes: LIKES,
124
+ tags: true
125
+ } } }
126
+ });
127
+ const toolIds = pins.map((p) => p.tool.id);
128
+ const ownerSlugs = pins.flatMap((p) => p.tool.owners.map((o) => o.personSlug));
129
+ const uniqueOwnerSlugs = Array.from(new Set(ownerSlugs));
130
+ const persons = await db.dbPerson.findMany({ where: { slug: { in: uniqueOwnerSlugs } } });
131
+ const personMap = new Map(persons.map((p) => [p.slug, p]));
132
+ const today = new Date(now);
133
+ today.setUTCHours(0, 0, 0, 0);
134
+ const thirtyDaysAgo = new Date(today);
135
+ thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 29);
136
+ const viewCounts = await Promise.all(toolIds.map(async (toolId) => {
137
+ const [views30d, viewsAll] = await Promise.all([db.dbCommunityToolView.count({ where: {
138
+ toolId,
139
+ day: { gte: thirtyDaysAgo }
140
+ } }), db.dbCommunityToolView.count({ where: { toolId } })]);
141
+ return {
142
+ toolId,
143
+ views30d,
144
+ viewsAll
145
+ };
146
+ }));
147
+ const viewCountMap = new Map(viewCounts.map((v) => [v.toolId, {
148
+ views30d: v.views30d,
149
+ viewsAll: v.viewsAll
150
+ }]));
151
+ return Promise.all(pins.map((pin) => toView(pin.tool, personMap, viewCountMap.get(pin.tool.id), user, isAdmin, now, capture)));
152
+ }
153
+ async function requireToolId(slug) {
154
+ const tool = await getDbClient().dbCommunityTool.findUnique({
155
+ where: { slug },
156
+ select: { id: true }
157
+ });
158
+ if (!tool) throw new TRPCError({
159
+ code: "NOT_FOUND",
160
+ message: "Tool not found"
161
+ });
162
+ return tool.id;
163
+ }
164
+ /** Pinning a pinned tool leaves it where it is. */
165
+ async function pinTool(slug, userId) {
166
+ const db = getDbClient();
167
+ const toolId = await requireToolId(slug);
168
+ const maxPin = await db.dbCommunityToolPin.findFirst({
169
+ where: { userId },
170
+ orderBy: { position: "desc" }
171
+ });
172
+ await db.dbCommunityToolPin.upsert({
173
+ where: { userId_toolId: {
174
+ userId,
175
+ toolId
176
+ } },
177
+ create: {
178
+ userId,
179
+ toolId,
180
+ position: maxPin ? maxPin.position + 1 : 0
181
+ },
182
+ update: {}
183
+ });
184
+ }
185
+ /** Prisma's unique-constraint error. */
186
+ function isUniqueViolation(error) {
187
+ return error instanceof Error && "code" in error && error.code === "P2002";
188
+ }
189
+ /** Liking twice is one like. */
190
+ async function likeTool(slug, user) {
191
+ const toolId = await requireToolId(slug);
192
+ try {
193
+ await getDbClient().dbCommunityToolLike.upsert({
194
+ where: { toolId_userId: {
195
+ toolId,
196
+ userId: user.id
197
+ } },
198
+ create: {
199
+ toolId,
200
+ userId: user.id,
201
+ userName: user.name
202
+ },
203
+ update: {}
204
+ });
205
+ } catch (error) {
206
+ if (!isUniqueViolation(error)) throw error;
207
+ }
208
+ }
209
+ /** Unliking what was never liked changes nothing. */
210
+ async function unlikeTool(slug, userId) {
211
+ const toolId = await requireToolId(slug);
212
+ await getDbClient().dbCommunityToolLike.deleteMany({ where: {
213
+ toolId,
214
+ userId
215
+ } });
216
+ }
217
+ async function unpinTool(slug, userId) {
218
+ const toolId = await requireToolId(slug);
219
+ await getDbClient().dbCommunityToolPin.deleteMany({ where: {
220
+ userId,
221
+ toolId
222
+ } });
223
+ }
224
+ async function reorderPins(slugs, userId) {
225
+ const db = getDbClient();
226
+ const pins = await db.dbCommunityToolPin.findMany({
227
+ where: { userId },
228
+ include: { tool: true },
229
+ orderBy: { position: "asc" }
230
+ });
231
+ const slugToPin = new Map(pins.map((p) => [p.tool.slug, p]));
232
+ const namedPins = slugs.map((slug) => slugToPin.get(slug)).filter((p) => p !== void 0);
233
+ const unlistedPins = pins.filter((p) => !slugs.includes(p.tool.slug));
234
+ const newOrder = [...namedPins, ...unlistedPins];
235
+ await Promise.all(newOrder.map((pin, index) => db.dbCommunityToolPin.update({
236
+ where: { userId_toolId: {
237
+ userId,
238
+ toolId: pin.toolId
239
+ } },
240
+ data: { position: index }
241
+ })));
242
+ }
243
+ async function computeCanEdit(tool, user, isAdmin) {
244
+ if (!user) return false;
245
+ if (isAdmin) return true;
246
+ if (tool.addedByUserId === user.id) return true;
247
+ return (await getDbClient().dbCommunityToolOwner.findMany({ where: { toolId: tool.id } })).some((o) => o.personSlug === user.email);
248
+ }
249
+
250
+ //#endregion
251
+ export { computeCanEdit, countByApp, getTool, likeTool, listPins, listTools, pinTool, recordView, reorderPins, unlikeTool, unpinTool };
252
+ //# sourceMappingURL=service.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.mjs","names":[],"sources":["../../../src/modules/communityTools/service.ts"],"sourcesContent":["import { TRPCError } from '@trpc/server'\nimport type { User } from 'better-auth/types'\nimport { getDbClient } from '../../db/client'\nimport type { Actor } from '../visitor/visitorIdentity'\nimport type { CommunityTool } from '@igstack/app-catalog-shared-core'\nimport type { CaptureDeps } from './healthJob'\nimport { LIKES, toView } from './toView'\n\nexport async function listTools(\n user: User | null,\n isAdmin: boolean,\n now: Date,\n capture: CaptureDeps,\n): Promise<CommunityTool[]> {\n const db = getDbClient()\n\n const tools = await db.dbCommunityTool.findMany({\n include: {\n owners: true,\n namedOwners: true,\n likes: LIKES,\n tags: true,\n },\n })\n\n const toolIds = tools.map((t) => t.id)\n\n // Fetch all persons in one query\n const ownerSlugs = tools.flatMap((t) => t.owners.map((o) => o.personSlug))\n const uniqueOwnerSlugs = Array.from(new Set(ownerSlugs))\n const persons = await db.dbPerson.findMany({\n where: { slug: { in: uniqueOwnerSlugs } },\n })\n const personMap = new Map(persons.map((p) => [p.slug, p]))\n\n // Get view counts for all tools\n const today = new Date(now)\n today.setUTCHours(0, 0, 0, 0)\n const thirtyDaysAgo = new Date(today)\n thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 29)\n\n const viewCounts = await Promise.all(\n toolIds.map(async (toolId) => {\n const [views30d, viewsAll] = await Promise.all([\n db.dbCommunityToolView.count({\n where: { toolId, day: { gte: thirtyDaysAgo } },\n }),\n db.dbCommunityToolView.count({ where: { toolId } }),\n ])\n return { toolId, views30d, viewsAll }\n }),\n )\n const viewCountMap = new Map(\n viewCounts.map((v) => [\n v.toolId,\n { views30d: v.views30d, viewsAll: v.viewsAll },\n ]),\n )\n\n return Promise.all(\n tools.map((tool) =>\n toView(\n tool,\n personMap,\n viewCountMap.get(tool.id)!,\n user,\n isAdmin,\n now,\n capture,\n ),\n ),\n )\n}\n\n/** How many live tools carry each official app's tag, by app slug. */\nexport async function countByApp(): Promise<Record<string, number>> {\n const rows = await getDbClient().dbCommunityToolTag.groupBy({\n by: ['value'],\n where: { kind: 'app', tool: { archivedAt: null } },\n _count: { _all: true },\n })\n return Object.fromEntries(rows.map((r) => [r.value, r._count._all]))\n}\n\nexport async function getTool(\n slug: string,\n user: User | null,\n isAdmin: boolean,\n now: Date,\n capture: CaptureDeps,\n): Promise<CommunityTool | null> {\n const db = getDbClient()\n\n const tool = await db.dbCommunityTool.findUnique({\n where: { slug },\n include: {\n owners: true,\n namedOwners: true,\n likes: LIKES,\n tags: true,\n },\n })\n\n if (!tool) return null\n\n const ownerSlugs = tool.owners.map((o) => o.personSlug)\n const persons = await db.dbPerson.findMany({\n where: { slug: { in: ownerSlugs } },\n })\n const personMap = new Map(persons.map((p) => [p.slug, p]))\n\n const today = new Date(now)\n today.setUTCHours(0, 0, 0, 0)\n const thirtyDaysAgo = new Date(today)\n thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 29)\n\n const [views30d, viewsAll] = await Promise.all([\n db.dbCommunityToolView.count({\n where: { toolId: tool.id, day: { gte: thirtyDaysAgo } },\n }),\n db.dbCommunityToolView.count({ where: { toolId: tool.id } }),\n ])\n\n return toView(\n tool,\n personMap,\n { views30d, viewsAll },\n user,\n isAdmin,\n now,\n capture,\n )\n}\n\nexport async function recordView(\n slug: string,\n actor: Actor,\n user: User | null,\n now: Date,\n): Promise<{ views30d: number; viewsAll: number }> {\n const db = getDbClient()\n\n const tool = await db.dbCommunityTool.findUnique({ where: { slug } })\n if (!tool) {\n throw new Error('Tool not found')\n }\n\n const day = new Date(now)\n day.setUTCHours(0, 0, 0, 0)\n\n await db.dbCommunityToolView.upsert({\n where: {\n toolId_actorHash_day: {\n toolId: tool.id,\n actorHash: actor.hash,\n day,\n },\n },\n create: {\n toolId: tool.id,\n actorHash: actor.hash,\n userId: user?.id ?? null,\n day,\n at: now,\n },\n update: {\n at: now,\n userId: user?.id ?? null,\n },\n })\n\n const thirtyDaysAgo = new Date(day)\n thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 29)\n\n const [views30d, viewsAll] = await Promise.all([\n db.dbCommunityToolView.count({\n where: { toolId: tool.id, day: { gte: thirtyDaysAgo } },\n }),\n db.dbCommunityToolView.count({ where: { toolId: tool.id } }),\n ])\n\n return { views30d, viewsAll }\n}\n\nexport async function listPins(\n userId: string,\n user: User | null,\n isAdmin: boolean,\n now: Date,\n capture: CaptureDeps,\n): Promise<CommunityTool[]> {\n const db = getDbClient()\n\n const pins = await db.dbCommunityToolPin.findMany({\n where: { userId },\n orderBy: { position: 'asc' },\n include: {\n tool: {\n include: {\n owners: true,\n namedOwners: true,\n likes: LIKES,\n tags: true,\n },\n },\n },\n })\n\n const toolIds = pins.map((p) => p.tool.id)\n\n const ownerSlugs = pins.flatMap((p) => p.tool.owners.map((o) => o.personSlug))\n const uniqueOwnerSlugs = Array.from(new Set(ownerSlugs))\n const persons = await db.dbPerson.findMany({\n where: { slug: { in: uniqueOwnerSlugs } },\n })\n const personMap = new Map(persons.map((p) => [p.slug, p]))\n\n const today = new Date(now)\n today.setUTCHours(0, 0, 0, 0)\n const thirtyDaysAgo = new Date(today)\n thirtyDaysAgo.setUTCDate(thirtyDaysAgo.getUTCDate() - 29)\n\n const viewCounts = await Promise.all(\n toolIds.map(async (toolId) => {\n const [views30d, viewsAll] = await Promise.all([\n db.dbCommunityToolView.count({\n where: { toolId, day: { gte: thirtyDaysAgo } },\n }),\n db.dbCommunityToolView.count({ where: { toolId } }),\n ])\n return { toolId, views30d, viewsAll }\n }),\n )\n const viewCountMap = new Map(\n viewCounts.map((v) => [\n v.toolId,\n { views30d: v.views30d, viewsAll: v.viewsAll },\n ]),\n )\n\n return Promise.all(\n pins.map((pin) =>\n toView(\n pin.tool,\n personMap,\n viewCountMap.get(pin.tool.id)!,\n user,\n isAdmin,\n now,\n capture,\n ),\n ),\n )\n}\n\nasync function requireToolId(slug: string): Promise<string> {\n const tool = await getDbClient().dbCommunityTool.findUnique({\n where: { slug },\n select: { id: true },\n })\n if (!tool) {\n throw new TRPCError({ code: 'NOT_FOUND', message: 'Tool not found' })\n }\n return tool.id\n}\n\n/** Pinning a pinned tool leaves it where it is. */\nexport async function pinTool(slug: string, userId: string): Promise<void> {\n const db = getDbClient()\n const toolId = await requireToolId(slug)\n\n const maxPin = await db.dbCommunityToolPin.findFirst({\n where: { userId },\n orderBy: { position: 'desc' },\n })\n\n await db.dbCommunityToolPin.upsert({\n where: { userId_toolId: { userId, toolId } },\n create: { userId, toolId, position: maxPin ? maxPin.position + 1 : 0 },\n update: {},\n })\n}\n\n/** Prisma's unique-constraint error. */\nfunction isUniqueViolation(error: unknown): boolean {\n return (\n error instanceof Error &&\n 'code' in error &&\n (error as { code: unknown }).code === 'P2002'\n )\n}\n\n/** Liking twice is one like. */\nexport async function likeTool(\n slug: string,\n user: { id: string; name: string },\n): Promise<void> {\n const toolId = await requireToolId(slug)\n try {\n await getDbClient().dbCommunityToolLike.upsert({\n where: { toolId_userId: { toolId, userId: user.id } },\n create: { toolId, userId: user.id, userName: user.name },\n update: {},\n })\n } catch (error) {\n // Two likes at once both miss, and one insert loses the race: still liked.\n if (!isUniqueViolation(error)) throw error\n }\n}\n\n/** Unliking what was never liked changes nothing. */\nexport async function unlikeTool(slug: string, userId: string): Promise<void> {\n const toolId = await requireToolId(slug)\n await getDbClient().dbCommunityToolLike.deleteMany({\n where: { toolId, userId },\n })\n}\n\nexport async function unpinTool(slug: string, userId: string): Promise<void> {\n const toolId = await requireToolId(slug)\n await getDbClient().dbCommunityToolPin.deleteMany({\n where: { userId, toolId },\n })\n}\n\nexport async function reorderPins(\n slugs: string[],\n userId: string,\n): Promise<void> {\n const db = getDbClient()\n\n // Get all pins for this user\n const pins = await db.dbCommunityToolPin.findMany({\n where: { userId },\n include: { tool: true },\n orderBy: { position: 'asc' },\n })\n\n // Create a map of slug to pin\n const slugToPin = new Map(pins.map((p) => [p.tool.slug, p]))\n\n // Build new order: named slugs first, then unlisted pins in current order\n const namedPins = slugs\n .map((slug) => slugToPin.get(slug))\n .filter((p) => p !== undefined)\n const unlistedPins = pins.filter((p) => !slugs.includes(p.tool.slug))\n const newOrder = [...namedPins, ...unlistedPins]\n\n // Update positions\n await Promise.all(\n newOrder.map((pin, index) =>\n db.dbCommunityToolPin.update({\n where: {\n userId_toolId: {\n userId,\n toolId: pin.toolId,\n },\n },\n data: { position: index },\n }),\n ),\n )\n}\n\nexport async function computeCanEdit(\n tool: { id: string; addedByUserId: string },\n user: User | null,\n isAdmin: boolean,\n): Promise<boolean> {\n if (!user) return false\n if (isAdmin) return true\n if (tool.addedByUserId === user.id) return true\n\n const db = getDbClient()\n const owners = await db.dbCommunityToolOwner.findMany({\n where: { toolId: tool.id },\n })\n\n return owners.some((o: { personSlug: string }) => o.personSlug === user.email)\n}\n"],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import { computeCanEdit } from "./service.mjs";
2
+ import { isCaptureAllowed, lifecycleOf } from "@igstack/app-catalog-shared-core";
3
+
4
+ //#region src/modules/communityTools/toView.ts
5
+ /** The likes a view reads: newest first, for "Liked by". */
6
+ const LIKES = {
7
+ orderBy: { createdAt: "desc" },
8
+ select: {
9
+ userId: true,
10
+ userName: true
11
+ }
12
+ };
13
+ /** How many likers a view names; the rest are a count. */
14
+ const LIKED_BY_NAMES = 8;
15
+ async function toView(tool, personMap, viewCounts, user, isAdmin, now, capture) {
16
+ const owners = tool.owners.map((o) => {
17
+ const person = personMap.get(o.personSlug);
18
+ if (person) {
19
+ const name = `${person.firstName} ${person.lastName}`.trim();
20
+ return {
21
+ slug: o.personSlug,
22
+ name: name || o.personSlug
23
+ };
24
+ }
25
+ return {
26
+ slug: o.personSlug,
27
+ name: o.personSlug
28
+ };
29
+ });
30
+ for (const o of tool.namedOwners) owners.push({
31
+ slug: null,
32
+ name: o.name
33
+ });
34
+ const tags = tool.tags.map((t) => ({
35
+ kind: t.kind,
36
+ value: t.value
37
+ }));
38
+ const lifecycle = lifecycleOf({
39
+ failingSince: tool.failingSince,
40
+ archivedAt: tool.archivedAt,
41
+ archivedBy: tool.archivedBy,
42
+ archivedReason: tool.archivedReason
43
+ }, now);
44
+ const capturePending = !tool.screenshotAssetId && !tool.lastCheckAt && capture.capturer !== null && isCaptureAllowed(tool.url, capture.captureHosts);
45
+ const canEdit = await computeCanEdit({
46
+ id: tool.id,
47
+ addedByUserId: tool.addedByUserId
48
+ }, user, isAdmin);
49
+ return {
50
+ slug: tool.slug,
51
+ name: tool.name,
52
+ url: tool.url,
53
+ host: tool.host,
54
+ summary: tool.summary,
55
+ descriptionMd: tool.descriptionMd,
56
+ owners,
57
+ addedBy: {
58
+ userId: tool.addedByUserId,
59
+ name: tool.addedByName
60
+ },
61
+ createdAt: tool.createdAt.toISOString(),
62
+ likes: tool.likes.length,
63
+ likedByMe: !!user && tool.likes.some((l) => l.userId === user.id),
64
+ likedBy: tool.likes.filter((l) => l.userId !== user?.id).slice(0, LIKED_BY_NAMES).map((l) => l.userName),
65
+ siteUpdatedAt: tool.siteUpdatedAt?.toISOString() ?? null,
66
+ tags,
67
+ views30d: viewCounts.views30d,
68
+ viewsAll: viewCounts.viewsAll,
69
+ reviewed: tool.reviewedAt !== null,
70
+ lifecycle,
71
+ screenshotId: tool.screenshotAssetId ?? null,
72
+ faviconId: tool.faviconAssetId ?? null,
73
+ capturedAt: tool.capturedAt?.toISOString() ?? null,
74
+ capturePending,
75
+ canEdit
76
+ };
77
+ }
78
+
79
+ //#endregion
80
+ export { LIKES, toView };
81
+ //# sourceMappingURL=toView.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toView.mjs","names":[],"sources":["../../../src/modules/communityTools/toView.ts"],"sourcesContent":["import type { User } from 'better-auth/types'\nimport type {\n CommunityTag,\n CommunityTool,\n PersonRef,\n} from '@igstack/app-catalog-shared-core'\nimport { isCaptureAllowed, lifecycleOf } from '@igstack/app-catalog-shared-core'\nimport type { DbPerson, Prisma } from '../../generated/prisma/client'\nimport type { CaptureDeps } from './healthJob'\nimport { computeCanEdit } from './service'\n\n/** The likes a view reads: newest first, for \"Liked by\". */\nexport const LIKES = {\n orderBy: { createdAt: 'desc' },\n select: { userId: true, userName: true },\n} as const satisfies Prisma.DbCommunityTool$likesArgs\n\n/** How many likers a view names; the rest are a count. */\nconst LIKED_BY_NAMES = 8\n\ntype DbToolWithRelations = Prisma.DbCommunityToolGetPayload<{\n include: {\n owners: true\n namedOwners: true\n tags: true\n likes: typeof LIKES\n }\n}>\n\nexport async function toView(\n tool: DbToolWithRelations,\n personMap: Map<string, DbPerson>,\n viewCounts: { views30d: number; viewsAll: number },\n user: User | null,\n isAdmin: boolean,\n now: Date,\n capture: CaptureDeps,\n): Promise<CommunityTool> {\n const owners: PersonRef[] = tool.owners.map((o) => {\n const person = personMap.get(o.personSlug)\n if (person) {\n const name = `${person.firstName} ${person.lastName}`.trim()\n return { slug: o.personSlug, name: name || o.personSlug }\n }\n return { slug: o.personSlug, name: o.personSlug }\n })\n // Outside the directory: a name to ask for, never an editor.\n for (const o of tool.namedOwners) owners.push({ slug: null, name: o.name })\n\n const tags: CommunityTag[] = tool.tags.map((t) => ({\n kind: t.kind,\n value: t.value,\n }))\n\n const lifecycle = lifecycleOf(\n {\n failingSince: tool.failingSince,\n archivedAt: tool.archivedAt,\n archivedBy: tool.archivedBy,\n archivedReason: tool.archivedReason,\n },\n now,\n )\n\n // Never visited yet, and a visit will come: the same conditions `checkTool`\n // runs under. A finished visit sets `lastCheckAt`, failed or not.\n const capturePending =\n !tool.screenshotAssetId &&\n !tool.lastCheckAt &&\n capture.capturer !== null &&\n isCaptureAllowed(tool.url, capture.captureHosts)\n\n const canEdit = await computeCanEdit(\n { id: tool.id, addedByUserId: tool.addedByUserId },\n user,\n isAdmin,\n )\n\n return {\n slug: tool.slug,\n name: tool.name,\n url: tool.url,\n host: tool.host,\n summary: tool.summary,\n descriptionMd: tool.descriptionMd,\n owners,\n addedBy: {\n userId: tool.addedByUserId,\n name: tool.addedByName,\n },\n createdAt: tool.createdAt.toISOString(),\n // ponytail: every like row is read per view; a counted column if likes run to thousands.\n likes: tool.likes.length,\n likedByMe: !!user && tool.likes.some((l) => l.userId === user.id),\n // The viewer's own like is `likedByMe`; the names are everyone else's.\n likedBy: tool.likes\n .filter((l) => l.userId !== user?.id)\n .slice(0, LIKED_BY_NAMES)\n .map((l) => l.userName),\n siteUpdatedAt: tool.siteUpdatedAt?.toISOString() ?? null,\n tags,\n views30d: viewCounts.views30d,\n viewsAll: viewCounts.viewsAll,\n reviewed: tool.reviewedAt !== null,\n lifecycle,\n screenshotId: tool.screenshotAssetId ?? null,\n faviconId: tool.faviconAssetId ?? null,\n capturedAt: tool.capturedAt?.toISOString() ?? null,\n capturePending,\n canEdit,\n }\n}\n"],"mappings":";;;;;AAYA,MAAa,QAAQ;CACnB,SAAS,EAAE,WAAW,QAAQ;CAC9B,QAAQ;EAAE,QAAQ;EAAM,UAAU;EAAM;CACzC;;AAGD,MAAM,iBAAiB;AAWvB,eAAsB,OACpB,MACA,WACA,YACA,MACA,SACA,KACA,SACwB;CACxB,MAAM,SAAsB,KAAK,OAAO,KAAK,MAAM;EACjD,MAAM,SAAS,UAAU,IAAI,EAAE,WAAW;AAC1C,MAAI,QAAQ;GACV,MAAM,OAAO,GAAG,OAAO,UAAU,GAAG,OAAO,WAAW,MAAM;AAC5D,UAAO;IAAE,MAAM,EAAE;IAAY,MAAM,QAAQ,EAAE;IAAY;;AAE3D,SAAO;GAAE,MAAM,EAAE;GAAY,MAAM,EAAE;GAAY;GACjD;AAEF,MAAK,MAAM,KAAK,KAAK,YAAa,QAAO,KAAK;EAAE,MAAM;EAAM,MAAM,EAAE;EAAM,CAAC;CAE3E,MAAM,OAAuB,KAAK,KAAK,KAAK,OAAO;EACjD,MAAM,EAAE;EACR,OAAO,EAAE;EACV,EAAE;CAEH,MAAM,YAAY,YAChB;EACE,cAAc,KAAK;EACnB,YAAY,KAAK;EACjB,YAAY,KAAK;EACjB,gBAAgB,KAAK;EACtB,EACD,IACD;CAID,MAAM,iBACJ,CAAC,KAAK,qBACN,CAAC,KAAK,eACN,QAAQ,aAAa,QACrB,iBAAiB,KAAK,KAAK,QAAQ,aAAa;CAElD,MAAM,UAAU,MAAM,eACpB;EAAE,IAAI,KAAK;EAAI,eAAe,KAAK;EAAe,EAClD,MACA,QACD;AAED,QAAO;EACL,MAAM,KAAK;EACX,MAAM,KAAK;EACX,KAAK,KAAK;EACV,MAAM,KAAK;EACX,SAAS,KAAK;EACd,eAAe,KAAK;EACpB;EACA,SAAS;GACP,QAAQ,KAAK;GACb,MAAM,KAAK;GACZ;EACD,WAAW,KAAK,UAAU,aAAa;EAEvC,OAAO,KAAK,MAAM;EAClB,WAAW,CAAC,CAAC,QAAQ,KAAK,MAAM,MAAM,MAAM,EAAE,WAAW,KAAK,GAAG;EAEjE,SAAS,KAAK,MACX,QAAQ,MAAM,EAAE,WAAW,MAAM,GAAG,CACpC,MAAM,GAAG,eAAe,CACxB,KAAK,MAAM,EAAE,SAAS;EACzB,eAAe,KAAK,eAAe,aAAa,IAAI;EACpD;EACA,UAAU,WAAW;EACrB,UAAU,WAAW;EACrB,UAAU,KAAK,eAAe;EAC9B;EACA,cAAc,KAAK,qBAAqB;EACxC,WAAW,KAAK,kBAAkB;EAClC,YAAY,KAAK,YAAY,aAAa,IAAI;EAC9C;EACA;EACD"}
@@ -0,0 +1,55 @@
1
+ import { Actor } from "../visitor/visitorIdentity.mjs";
2
+ import { z } from "zod";
3
+ import * as _igstack_app_catalog_shared_core0 from "@igstack/app-catalog-shared-core";
4
+ import { User } from "better-auth/types";
5
+
6
+ //#region src/modules/communityTools/writeService.d.ts
7
+ declare const toolInputSchema: z.ZodObject<{
8
+ url: z.ZodString;
9
+ name: z.ZodString;
10
+ summary: z.ZodString;
11
+ descriptionMd: z.ZodString;
12
+ ownerSlugs: z.ZodArray<z.ZodString>;
13
+ ownerNames: z.ZodOptional<z.ZodPipe<z.ZodArray<z.ZodString>, z.ZodTransform<string[], string[]>>>;
14
+ tags: z.ZodPipe<z.ZodPipe<z.ZodArray<z.ZodObject<{
15
+ kind: z.ZodEnum<{
16
+ app: "app";
17
+ topic: "topic";
18
+ }>;
19
+ value: z.ZodString;
20
+ }, z.core.$strip>>, z.ZodTransform<_igstack_app_catalog_shared_core0.CommunityTag[], {
21
+ kind: "app" | "topic";
22
+ value: string;
23
+ }[]>>, z.ZodArray<z.ZodObject<{
24
+ kind: z.ZodEnum<{
25
+ app: "app";
26
+ topic: "topic";
27
+ }>;
28
+ value: z.ZodString;
29
+ }, z.core.$strip>>>;
30
+ }, z.core.$strip>;
31
+ type ToolInput = z.output<typeof toolInputSchema>;
32
+ /** The signed-in person making the change. */
33
+ interface Writer {
34
+ user: User;
35
+ isAdmin: boolean;
36
+ actor: Actor;
37
+ }
38
+ interface ToolPreview {
39
+ host: string;
40
+ title: string | null;
41
+ screenshotDataUrl: string | null;
42
+ duplicate: {
43
+ slug: string;
44
+ name: string;
45
+ } | null;
46
+ /** An official app on the same host: the tool would duplicate a catalog entry. */
47
+ officialApp: {
48
+ slug: string;
49
+ name: string;
50
+ } | null;
51
+ captureAllowed: boolean;
52
+ }
53
+ //#endregion
54
+ export { ToolInput, ToolPreview, Writer, toolInputSchema };
55
+ //# sourceMappingURL=writeService.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writeService.d.mts","names":[],"sources":["../../../src/modules/communityTools/writeService.ts"],"mappings":";;;;;;cAkBa,eAAA,EAAe,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;KA+BhB,SAAA,GAAY,CAAA,CAAE,MAAA,QAAc,eAAA;;UAKvB,MAAA;EACf,IAAA,EAAM,IAAA;EACN,OAAA;EACA,KAAA,EAAO,KAAA;AAAA;AAAA,UAGQ,WAAA;EACf,IAAA;EACA,KAAA;EACA,iBAAA;EACA,SAAA;IAAa,IAAA;IAAc,IAAA;EAAA;;EAE3B,WAAA;IAAe,IAAA;IAAc,IAAA;EAAA;EAC7B,cAAA;AAAA"}