@igstack/app-catalog-backend-core 6.3.1-alpha-20261003051755 → 6.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.
Files changed (106) hide show
  1. package/dist/db/syncAppCatalog.mjs +1 -0
  2. package/dist/db/syncAppCatalog.mjs.map +1 -1
  3. package/dist/generated/prisma/client.mjs.map +1 -1
  4. package/dist/generated/prisma/commonInputTypes.d.mts +72 -72
  5. package/dist/generated/prisma/commonInputTypes.d.mts.map +1 -1
  6. package/dist/generated/prisma/internal/class.d.mts +17 -5
  7. package/dist/generated/prisma/internal/class.d.mts.map +1 -1
  8. package/dist/generated/prisma/internal/class.mjs +4 -4
  9. package/dist/generated/prisma/internal/class.mjs.map +1 -1
  10. package/dist/generated/prisma/internal/prismaNamespace.d.mts +145 -51
  11. package/dist/generated/prisma/internal/prismaNamespace.d.mts.map +1 -1
  12. package/dist/generated/prisma/models/DbApprovalMethod.d.mts +30 -3
  13. package/dist/generated/prisma/models/DbApprovalMethod.d.mts.map +1 -1
  14. package/dist/generated/prisma/models/DbAsset.d.mts +2 -11
  15. package/dist/generated/prisma/models/DbAsset.d.mts.map +1 -1
  16. package/dist/generated/prisma/models/DbFeedback.d.mts +1565 -0
  17. package/dist/generated/prisma/models/DbFeedback.d.mts.map +1 -0
  18. package/dist/generated/prisma/models/DbFeedbackAttachment.d.mts +1431 -0
  19. package/dist/generated/prisma/models/DbFeedbackAttachment.d.mts.map +1 -0
  20. package/dist/generated/prisma/models/DbResource.d.mts +108 -60
  21. package/dist/generated/prisma/models/DbResource.d.mts.map +1 -1
  22. package/dist/generated/prisma/models/SourceReference.d.mts +1 -1
  23. package/dist/generated/prisma/models.d.mts +4 -3
  24. package/dist/index.d.mts +5 -3
  25. package/dist/index.mjs +5 -3
  26. package/dist/middleware/createAcMiddleware.mjs +2 -2
  27. package/dist/middleware/createAcMiddleware.mjs.map +1 -1
  28. package/dist/middleware/featureRegistry.mjs +3 -1
  29. package/dist/middleware/featureRegistry.mjs.map +1 -1
  30. package/dist/modules/appCatalog/service.d.mts.map +1 -1
  31. package/dist/modules/appCatalog/service.mjs +1 -0
  32. package/dist/modules/appCatalog/service.mjs.map +1 -1
  33. package/dist/modules/feedback/feedbackAttachmentController.d.mts +22 -0
  34. package/dist/modules/feedback/feedbackAttachmentController.d.mts.map +1 -0
  35. package/dist/modules/feedback/feedbackAttachmentController.mjs +192 -0
  36. package/dist/modules/feedback/feedbackAttachmentController.mjs.map +1 -0
  37. package/dist/modules/feedback/feedbackRouter.d.mts +54 -0
  38. package/dist/modules/feedback/feedbackRouter.d.mts.map +1 -0
  39. package/dist/modules/feedback/feedbackRouter.mjs +59 -0
  40. package/dist/modules/feedback/feedbackRouter.mjs.map +1 -0
  41. package/dist/modules/feedback/service.d.mts +42 -0
  42. package/dist/modules/feedback/service.d.mts.map +1 -0
  43. package/dist/modules/feedback/service.mjs +165 -0
  44. package/dist/modules/feedback/service.mjs.map +1 -0
  45. package/dist/modules/visitor/visitorIdentity.d.mts +86 -0
  46. package/dist/modules/visitor/visitorIdentity.d.mts.map +1 -0
  47. package/dist/modules/{comments → visitor}/visitorIdentity.mjs +53 -2
  48. package/dist/modules/visitor/visitorIdentity.mjs.map +1 -0
  49. package/dist/server/acTrpcContext.d.mts +8 -7
  50. package/dist/server/acTrpcContext.d.mts.map +1 -1
  51. package/dist/server/acTrpcContext.mjs +2 -2
  52. package/dist/server/acTrpcContext.mjs.map +1 -1
  53. package/dist/server/controller.d.mts +15 -8
  54. package/dist/server/controller.d.mts.map +1 -1
  55. package/dist/server/controller.mjs +2 -2
  56. package/dist/server/controller.mjs.map +1 -1
  57. package/dist/types/common/appCatalogTypes.d.mts +12 -0
  58. package/dist/types/common/appCatalogTypes.d.mts.map +1 -1
  59. package/dist/types/common/approvalMethodTypes.d.mts +12 -0
  60. package/dist/types/common/approvalMethodTypes.d.mts.map +1 -1
  61. package/package.json +5 -5
  62. package/prisma/migrations/20261004000000_feedback_replaces_comments/migration.sql +74 -0
  63. package/prisma/schema.prisma +84 -24
  64. package/src/__tests__/feedbackAttachmentUpload.test.ts +192 -0
  65. package/src/__tests__/{comments.test.ts → visitorIdentity.test.ts} +1 -1
  66. package/src/db/syncAppCatalog.ts +1 -0
  67. package/src/generated/prisma/browser.ts +22 -7
  68. package/src/generated/prisma/client.ts +22 -7
  69. package/src/generated/prisma/commonInputTypes.ts +85 -85
  70. package/src/generated/prisma/internal/class.ts +18 -8
  71. package/src/generated/prisma/internal/prismaNamespace.ts +143 -47
  72. package/src/generated/prisma/internal/prismaNamespaceBrowser.ts +24 -3
  73. package/src/generated/prisma/models/DbApprovalMethod.ts +29 -1
  74. package/src/generated/prisma/models/DbAsset.ts +0 -12
  75. package/src/generated/prisma/models/DbFeedback.ts +1814 -0
  76. package/src/generated/prisma/models/DbFeedbackAttachment.ts +1661 -0
  77. package/src/generated/prisma/models/DbResource.ts +107 -58
  78. package/src/generated/prisma/models.ts +2 -1
  79. package/src/index.ts +24 -4
  80. package/src/middleware/createAcMiddleware.ts +4 -3
  81. package/src/middleware/featureRegistry.ts +7 -0
  82. package/src/modules/appCatalog/service.ts +1 -0
  83. package/src/modules/feedback/feedbackAttachmentController.ts +248 -0
  84. package/src/modules/feedback/feedbackRouter.ts +102 -0
  85. package/src/modules/feedback/service.ts +295 -0
  86. package/src/modules/{comments → visitor}/visitorIdentity.ts +84 -0
  87. package/src/server/acTrpcContext.ts +9 -8
  88. package/src/server/controller.ts +2 -2
  89. package/src/types/common/appCatalogTypes.ts +12 -0
  90. package/src/types/common/approvalMethodTypes.ts +12 -0
  91. package/dist/generated/prisma/models/DbComment.d.mts +0 -1340
  92. package/dist/generated/prisma/models/DbComment.d.mts.map +0 -1
  93. package/dist/modules/comments/commentsRouter.d.mts +0 -47
  94. package/dist/modules/comments/commentsRouter.d.mts.map +0 -1
  95. package/dist/modules/comments/commentsRouter.mjs +0 -55
  96. package/dist/modules/comments/commentsRouter.mjs.map +0 -1
  97. package/dist/modules/comments/service.d.mts +0 -40
  98. package/dist/modules/comments/service.d.mts.map +0 -1
  99. package/dist/modules/comments/service.mjs +0 -88
  100. package/dist/modules/comments/service.mjs.map +0 -1
  101. package/dist/modules/comments/visitorIdentity.d.mts +0 -15
  102. package/dist/modules/comments/visitorIdentity.d.mts.map +0 -1
  103. package/dist/modules/comments/visitorIdentity.mjs.map +0 -1
  104. package/src/generated/prisma/models/DbComment.ts +0 -1561
  105. package/src/modules/comments/commentsRouter.ts +0 -85
  106. package/src/modules/comments/service.ts +0 -174
@@ -1,85 +0,0 @@
1
- import { TRPCError } from '@trpc/server'
2
- import { z } from 'zod'
3
- import { publicProcedure, router } from '../../server/trpcSetup'
4
- import type { Visitor } from './visitorIdentity'
5
- import {
6
- CommentError,
7
- MAX_BODY_LENGTH,
8
- addComment,
9
- deleteComment,
10
- editComment,
11
- listComments,
12
- } from './service'
13
-
14
- const bodyInput = z.string().min(1).max(MAX_BODY_LENGTH)
15
-
16
- /**
17
- * Writing needs a visitor token, and the token is issued on the request that reads
18
- * the comment list — so by the time anyone can click Post, the cookie exists. A
19
- * missing one here means cookies are blocked, which no error code describes well;
20
- * PRECONDITION_FAILED at least separates it from a permission problem.
21
- */
22
- function requireVisitor(visitor: Visitor | null): Visitor {
23
- if (!visitor) {
24
- throw new TRPCError({
25
- code: 'PRECONDITION_FAILED',
26
- message: 'Commenting needs cookies enabled',
27
- })
28
- }
29
- return visitor
30
- }
31
-
32
- const REASON_CODES = {
33
- 'not-found': 'NOT_FOUND',
34
- 'not-author': 'FORBIDDEN',
35
- 'window-closed': 'FORBIDDEN',
36
- empty: 'BAD_REQUEST',
37
- } as const
38
-
39
- async function mapErrors<T>(work: () => Promise<T>): Promise<T> {
40
- try {
41
- return await work()
42
- } catch (error) {
43
- if (error instanceof CommentError) {
44
- throw new TRPCError({
45
- code: REASON_CODES[error.reason],
46
- message: error.message,
47
- })
48
- }
49
- throw error
50
- }
51
- }
52
-
53
- export function createCommentsRouter() {
54
- return router({
55
- list: publicProcedure
56
- .input(z.object({ resourceSlug: z.string() }))
57
- .query(({ input, ctx }) => listComments(input.resourceSlug, ctx.visitor)),
58
-
59
- add: publicProcedure
60
- .input(z.object({ resourceSlug: z.string(), body: bodyInput }))
61
- .mutation(({ input, ctx }) =>
62
- mapErrors(() =>
63
- addComment(
64
- input.resourceSlug,
65
- input.body,
66
- requireVisitor(ctx.visitor),
67
- ),
68
- ),
69
- ),
70
-
71
- edit: publicProcedure
72
- .input(z.object({ id: z.string(), body: bodyInput }))
73
- .mutation(({ input, ctx }) =>
74
- mapErrors(() =>
75
- editComment(input.id, input.body, requireVisitor(ctx.visitor)),
76
- ),
77
- ),
78
-
79
- remove: publicProcedure
80
- .input(z.object({ id: z.string() }))
81
- .mutation(({ input, ctx }) =>
82
- mapErrors(() => deleteComment(input.id, requireVisitor(ctx.visitor))),
83
- ),
84
- })
85
- }
@@ -1,174 +0,0 @@
1
- import { getDbClient } from '../../db/client'
2
- import type { Visitor } from './visitorIdentity'
3
-
4
- /** How long an author may edit or delete their own comment. */
5
- export const EDIT_WINDOW_MS = 60 * 60 * 1000
6
-
7
- export const MAX_BODY_LENGTH = 1000
8
-
9
- /**
10
- * What a maintainer did with a comment. Null while nobody has looked at it.
11
- *
12
- * Mirrors the Prisma enum rather than importing it, so the view type stays
13
- * usable on the client.
14
- */
15
- export type CommentStatus = 'applied' | 'acknowledged'
16
-
17
- /** One comment as a client sees it. Never carries an author hash. */
18
- export interface CommentView {
19
- id: string
20
- authorName: string
21
- body: string
22
- createdAt: string
23
- /** Null when the comment has never been edited. */
24
- editedAt: string | null
25
- /**
26
- * Null until a maintainer reviews the comment. `applied` is deliberately
27
- * visible to everyone: it tells the next reader that comments reach a human
28
- * and change the catalog.
29
- */
30
- status: CommentStatus | null
31
- /** One answer from the maintainers, or null. */
32
- reviewerReply: string | null
33
- /** Whether this browser wrote it — what makes Edit and Delete appear. */
34
- isMine: boolean
35
- /**
36
- * The absolute instant this browser's edit window closes, or null when it is not
37
- * this browser's comment. Absolute rather than a remaining duration so a client
38
- * with a skewed clock still hides the controls at the same moment the server
39
- * starts refusing the edit.
40
- */
41
- canEditUntil: string | null
42
- }
43
-
44
- function toView(
45
- row: {
46
- id: string
47
- authorHash: string
48
- authorAlias: string
49
- body: string
50
- editedAt: Date | null
51
- createdAt: Date
52
- status?: CommentStatus | null
53
- reviewerReply?: string | null
54
- },
55
- visitor: Visitor | null,
56
- ): CommentView {
57
- const isMine = !!visitor && row.authorHash === visitor.hash
58
- return {
59
- id: row.id,
60
- authorName: row.authorAlias,
61
- body: row.body,
62
- createdAt: row.createdAt.toISOString(),
63
- editedAt: row.editedAt?.toISOString() ?? null,
64
- status: row.status ?? null,
65
- reviewerReply: row.reviewerReply ?? null,
66
- isMine,
67
- canEditUntil: isMine
68
- ? new Date(row.createdAt.getTime() + EDIT_WINDOW_MS).toISOString()
69
- : null,
70
- }
71
- }
72
-
73
- async function resourceIdForSlug(slug: string): Promise<string | null> {
74
- const resource = await getDbClient().dbResource.findUnique({
75
- where: { slug },
76
- select: { id: true },
77
- })
78
- return resource?.id ?? null
79
- }
80
-
81
- /** Oldest first: a comment thread reads top to bottom. */
82
- export async function listComments(
83
- resourceSlug: string,
84
- visitor: Visitor | null,
85
- ): Promise<CommentView[]> {
86
- const resourceId = await resourceIdForSlug(resourceSlug)
87
- if (!resourceId) return []
88
-
89
- const rows = await getDbClient().dbComment.findMany({
90
- where: { resourceId },
91
- orderBy: { createdAt: 'asc' },
92
- })
93
- return rows.map((row) => toView(row, visitor))
94
- }
95
-
96
- export class CommentError extends Error {
97
- constructor(
98
- readonly reason: 'not-found' | 'not-author' | 'window-closed' | 'empty',
99
- message: string,
100
- ) {
101
- super(message)
102
- }
103
- }
104
-
105
- function normaliseBody(body: string): string {
106
- const trimmed = body.trim()
107
- if (!trimmed) throw new CommentError('empty', 'A comment cannot be empty')
108
- return trimmed.slice(0, MAX_BODY_LENGTH)
109
- }
110
-
111
- export async function addComment(
112
- resourceSlug: string,
113
- body: string,
114
- visitor: Visitor,
115
- ): Promise<CommentView> {
116
- const resourceId = await resourceIdForSlug(resourceSlug)
117
- if (!resourceId) {
118
- throw new CommentError('not-found', `Unknown resource: ${resourceSlug}`)
119
- }
120
-
121
- const row = await getDbClient().dbComment.create({
122
- data: {
123
- resourceId,
124
- authorHash: visitor.hash,
125
- authorAlias: visitor.alias,
126
- body: normaliseBody(body),
127
- },
128
- })
129
- return toView(row, visitor)
130
- }
131
-
132
- /**
133
- * Load a comment and check this visitor may still change it.
134
- *
135
- * The window is enforced here, not in the UI: the controls disappearing is a
136
- * courtesy, and a request that arrives a second late must still be refused.
137
- */
138
- async function loadOwn(id: string, visitor: Visitor) {
139
- const row = await getDbClient().dbComment.findUnique({ where: { id } })
140
- if (!row) throw new CommentError('not-found', 'Comment no longer exists')
141
- if (row.authorHash !== visitor.hash) {
142
- throw new CommentError('not-author', 'Only the author can change a comment')
143
- }
144
- if (Date.now() > row.createdAt.getTime() + EDIT_WINDOW_MS) {
145
- throw new CommentError(
146
- 'window-closed',
147
- 'A comment can only be changed in the first hour',
148
- )
149
- }
150
- return row
151
- }
152
-
153
- export async function editComment(
154
- id: string,
155
- body: string,
156
- visitor: Visitor,
157
- ): Promise<CommentView> {
158
- await loadOwn(id, visitor)
159
-
160
- const row = await getDbClient().dbComment.update({
161
- where: { id },
162
- data: { body: normaliseBody(body), editedAt: new Date() },
163
- })
164
- return toView(row, visitor)
165
- }
166
-
167
- export async function deleteComment(
168
- id: string,
169
- visitor: Visitor,
170
- ): Promise<{ id: string }> {
171
- await loadOwn(id, visitor)
172
- await getDbClient().dbComment.delete({ where: { id } })
173
- return { id }
174
- }