@kernhq/module-quire 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.
Files changed (58) hide show
  1. package/dist/contract/events.d.ts +5 -0
  2. package/dist/contract/events.d.ts.map +1 -1
  3. package/dist/contract/events.js +1 -0
  4. package/dist/contract/events.js.map +1 -1
  5. package/dist/contract/index.d.ts +1 -0
  6. package/dist/contract/index.d.ts.map +1 -1
  7. package/dist/contract/index.js +1 -0
  8. package/dist/contract/index.js.map +1 -1
  9. package/dist/contract/models.d.ts +82 -0
  10. package/dist/contract/models.d.ts.map +1 -1
  11. package/dist/contract/models.js +41 -0
  12. package/dist/contract/models.js.map +1 -1
  13. package/dist/contract/notifications.d.ts +9 -0
  14. package/dist/contract/notifications.d.ts.map +1 -0
  15. package/dist/contract/notifications.js +16 -0
  16. package/dist/contract/notifications.js.map +1 -0
  17. package/dist/contract/permissions.d.ts +7 -0
  18. package/dist/contract/permissions.d.ts.map +1 -1
  19. package/dist/contract/permissions.js +8 -0
  20. package/dist/contract/permissions.js.map +1 -1
  21. package/dist/contract/router.d.ts +270 -0
  22. package/dist/contract/router.d.ts.map +1 -1
  23. package/dist/contract/router.js +33 -1
  24. package/dist/contract/router.js.map +1 -1
  25. package/dist/server/_impl.d.ts +303 -0
  26. package/dist/server/_impl.d.ts.map +1 -1
  27. package/dist/server/_impl.js +55 -0
  28. package/dist/server/_impl.js.map +1 -1
  29. package/dist/server/index.d.ts.map +1 -1
  30. package/dist/server/index.js +132 -3
  31. package/dist/server/index.js.map +1 -1
  32. package/dist/server/schema.d.ts +335 -1
  33. package/dist/server/schema.d.ts.map +1 -1
  34. package/dist/server/schema.js +44 -2
  35. package/dist/server/schema.js.map +1 -1
  36. package/dist/server/services/comments.d.ts +172 -0
  37. package/dist/server/services/comments.d.ts.map +1 -0
  38. package/dist/server/services/comments.js +182 -0
  39. package/dist/server/services/comments.js.map +1 -0
  40. package/dist/server/services/index.d.ts +3 -0
  41. package/dist/server/services/index.d.ts.map +1 -1
  42. package/dist/server/services/index.js +3 -0
  43. package/dist/server/services/index.js.map +1 -1
  44. package/dist/server/services/notify.d.ts +24 -0
  45. package/dist/server/services/notify.d.ts.map +1 -0
  46. package/dist/server/services/notify.js +54 -0
  47. package/dist/server/services/notify.js.map +1 -0
  48. package/migrations/0003_comments.sql +30 -0
  49. package/migrations/meta/0003_snapshot.json +668 -0
  50. package/migrations/meta/_journal.json +7 -0
  51. package/package.json +1 -1
  52. package/src/client/index.ts +2 -0
  53. package/src/contract/events.ts +4 -0
  54. package/src/contract/index.ts +1 -0
  55. package/src/contract/models.ts +48 -0
  56. package/src/contract/notifications.ts +17 -0
  57. package/src/contract/permissions.ts +8 -0
  58. package/src/contract/router.ts +47 -1
@@ -22,6 +22,10 @@ export const quireEvents = {
22
22
  'quire.page.restored_version',
23
23
  z.object({ pageId: Id, workspaceId: WorkspaceId, versionId: Id }),
24
24
  ),
25
+ commentCreated: defineEvent(
26
+ 'quire.comment.created',
27
+ z.object({ commentId: Id, pageId: Id, workspaceId: WorkspaceId }),
28
+ ),
25
29
  /** The page and its descendants are gone; anything holding a reference should drop it. */
26
30
  pageDeleted: defineEvent('quire.page.deleted', page.extend({ pageIds: z.array(Id) })),
27
31
  } as const
@@ -1,5 +1,6 @@
1
1
  export * from './capabilities.js'
2
2
  export * from './events.js'
3
3
  export * from './models.js'
4
+ export * from './notifications.js'
4
5
  export * from './permissions.js'
5
6
  export * from './router.js'
@@ -113,4 +113,52 @@ export const PageVersion = z.object({
113
113
  })
114
114
  export type PageVersion = z.infer<typeof PageVersion>
115
115
 
116
+ /**
117
+ * Where a comment is attached, as **Yjs relative positions**.
118
+ *
119
+ * Not character offsets: an offset names a place that only exists while nobody else is typing, and
120
+ * two words inserted above would move a comment onto text it was never about. A relative position
121
+ * points at the content rather than the index, so it survives concurrent editing — which is the
122
+ * whole reason a comment on a collaborative document is harder than a comment on a row.
123
+ */
124
+ export const CommentAnchor = z.object({
125
+ /** base64 `Y.encodeRelativePosition` */
126
+ from: z.base64(),
127
+ to: z.base64(),
128
+ })
129
+ export type CommentAnchor = z.infer<typeof CommentAnchor>
130
+
131
+ /** A Tiptap/ProseMirror document. Kept opaque here; the renderer is what knows its shape. */
132
+ export const RichDoc = z.record(z.string(), z.unknown())
133
+
134
+ export const Comment = z.object({
135
+ id: Id,
136
+ workspaceId: WorkspaceId,
137
+ pageId: Id,
138
+ parentId: Id.nullable(),
139
+ threadId: Id,
140
+ authorId: UserId.nullable(),
141
+ body: RichDoc,
142
+ bodyText: z.string(),
143
+ mentionIds: z.array(UserId),
144
+ /** null for a comment about the page rather than a piece of it */
145
+ anchor: CommentAnchor.nullable(),
146
+ /** what the anchor pointed at when it was written, so a thread whose text is gone still reads */
147
+ quotedText: z.string(),
148
+ resolvedAt: Timestamp.nullable(),
149
+ resolvedBy: UserId.nullable(),
150
+ editedAt: Timestamp.nullable(),
151
+ createdAt: Timestamp,
152
+ })
153
+ export type Comment = z.infer<typeof Comment>
154
+
155
+ /** A root comment and its replies, which is how a page's margin is actually read. */
156
+ export const CommentThread = z.object({
157
+ id: Id,
158
+ root: Comment,
159
+ replies: z.array(Comment),
160
+ resolved: z.boolean(),
161
+ })
162
+ export type CommentThread = z.infer<typeof CommentThread>
163
+
116
164
  export const Ok = z.object({ ok: z.literal(true) })
@@ -0,0 +1,17 @@
1
+ import type { core } from '@kernhq/contracts'
2
+
3
+ /**
4
+ * What Quire can put in somebody's inbox.
5
+ *
6
+ * Declared rather than invented at the call site: the inbox renders a notification from its type,
7
+ * and one that was never declared arrives as a blank row.
8
+ */
9
+ export const quireNotificationTypes: core.NotificationTypeDef[] = [
10
+ {
11
+ type: 'quire.mention',
12
+ label: 'Mentioned in a page comment',
13
+ description: 'Somebody named you in a remark on a page',
14
+ defaults: { inapp: true, push: true, email: false },
15
+ urgent: false,
16
+ },
17
+ ]
@@ -48,6 +48,14 @@ export const quirePermissions = definePermissions([
48
48
  defaultRoles: ['owner', 'admin', 'member'],
49
49
  dangerous: false,
50
50
  },
51
+ {
52
+ key: 'quire.page.comment',
53
+ label: 'Comment on pages',
54
+ description: 'Leave a remark in the margin without being able to change the page',
55
+ scope: 'space',
56
+ defaultRoles: ['owner', 'admin', 'member', 'guest'],
57
+ dangerous: false,
58
+ },
51
59
  {
52
60
  key: 'quire.page.publish',
53
61
  label: 'Publish pages',
@@ -1,6 +1,17 @@
1
1
  import { baseContract, Id, PageInput, page, WorkspaceId } from '@kernhq/contracts'
2
2
  import { z } from 'zod'
3
- import { Ok, Page, PageKind, PageNode, PageVersion, Space, SpaceVisibility } from './models.js'
3
+ import {
4
+ CommentAnchor,
5
+ CommentThread,
6
+ Ok,
7
+ Page,
8
+ PageKind,
9
+ PageNode,
10
+ PageVersion,
11
+ RichDoc,
12
+ Space,
13
+ SpaceVisibility,
14
+ } from './models.js'
4
15
 
5
16
  const ws = z.object({ workspaceId: WorkspaceId })
6
17
  const t = (...tags: string[]) => ({ tags })
@@ -141,6 +152,41 @@ export const quireContract = {
141
152
  .output(PageVersion),
142
153
  },
143
154
 
155
+ comments: {
156
+ /** Every open thread on a page, and optionally the resolved ones too. */
157
+ list: baseContract
158
+ .route({ method: 'GET', path: '/pages/{pageId}/comments', ...t('comments') })
159
+ .input(ws.extend({ pageId: Id, includeResolved: z.boolean().default(false) }))
160
+ .output(z.array(CommentThread)),
161
+ create: baseContract
162
+ .route({ method: 'POST', path: '/pages/{pageId}/comments', ...t('comments') })
163
+ .input(
164
+ ws.extend({
165
+ pageId: Id,
166
+ body: RichDoc,
167
+ /** omit for a comment about the page rather than a piece of it */
168
+ anchor: CommentAnchor.nullable().default(null),
169
+ quotedText: z.string().max(2000).default(''),
170
+ /** reply to this comment; the thread is inferred from it */
171
+ parentId: Id.nullable().default(null),
172
+ }),
173
+ )
174
+ .output(CommentThread.shape.root),
175
+ update: baseContract
176
+ .route({ method: 'PATCH', path: '/comments/{commentId}', ...t('comments') })
177
+ .input(ws.extend({ commentId: Id, body: RichDoc }))
178
+ .output(CommentThread.shape.root),
179
+ remove: baseContract
180
+ .route({ method: 'DELETE', path: '/comments/{commentId}', ...t('comments') })
181
+ .input(ws.extend({ commentId: Id }))
182
+ .output(Ok),
183
+ /** Settle a thread. Resolving the root resolves the thread; it is not a per-reply state. */
184
+ resolve: baseContract
185
+ .route({ method: 'POST', path: '/comments/{commentId}/resolve', ...t('comments') })
186
+ .input(ws.extend({ commentId: Id, resolved: z.boolean().default(true) }))
187
+ .output(CommentThread),
188
+ },
189
+
144
190
  publishing: {
145
191
  /** Make what is written now the version readers are served. Only meaningful for a `page`. */
146
192
  publish: baseContract