@kernhq/module-quire 0.10.8 → 0.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 (53) hide show
  1. package/dist/contract/models.d.ts +87 -0
  2. package/dist/contract/models.d.ts.map +1 -1
  3. package/dist/contract/models.js +73 -0
  4. package/dist/contract/models.js.map +1 -1
  5. package/dist/contract/permissions.d.ts.map +1 -1
  6. package/dist/contract/permissions.js +32 -0
  7. package/dist/contract/permissions.js.map +1 -1
  8. package/dist/contract/router.d.ts +645 -0
  9. package/dist/contract/router.d.ts.map +1 -1
  10. package/dist/contract/router.js +137 -2
  11. package/dist/contract/router.js.map +1 -1
  12. package/dist/server/_impl.d.ts +877 -0
  13. package/dist/server/_impl.d.ts.map +1 -1
  14. package/dist/server/_impl.js +127 -1
  15. package/dist/server/_impl.js.map +1 -1
  16. package/dist/server/schema.d.ts +482 -1
  17. package/dist/server/schema.d.ts.map +1 -1
  18. package/dist/server/schema.js +115 -1
  19. package/dist/server/schema.js.map +1 -1
  20. package/dist/server/services/index.d.ts +3 -0
  21. package/dist/server/services/index.d.ts.map +1 -1
  22. package/dist/server/services/index.js +3 -0
  23. package/dist/server/services/index.js.map +1 -1
  24. package/dist/server/services/organisation.d.ts +117 -0
  25. package/dist/server/services/organisation.d.ts.map +1 -0
  26. package/dist/server/services/organisation.js +319 -0
  27. package/dist/server/services/organisation.js.map +1 -0
  28. package/dist/server/services/pages.d.ts +16 -1
  29. package/dist/server/services/pages.d.ts.map +1 -1
  30. package/dist/server/services/pages.js +62 -3
  31. package/dist/server/services/pages.js.map +1 -1
  32. package/migrations/0007_organisation.sql +114 -0
  33. package/migrations/meta/0007_snapshot.json +1588 -0
  34. package/migrations/meta/_journal.json +7 -0
  35. package/package.json +1 -1
  36. package/src/client/components/ConfirmDialog.svelte +123 -0
  37. package/src/client/components/FavoriteStar.svelte +81 -0
  38. package/src/client/components/LabelChip.svelte +46 -0
  39. package/src/client/components/LabelManager.svelte +336 -0
  40. package/src/client/components/PageLabels.svelte +158 -0
  41. package/src/client/components/SidebarFavorites.svelte +262 -0
  42. package/src/client/components/SidebarRecents.svelte +98 -0
  43. package/src/client/components/SidebarSpaces.svelte +266 -1
  44. package/src/client/i18n.ts +387 -0
  45. package/src/client/index.ts +8 -0
  46. package/src/client/mock.ts +306 -0
  47. package/src/client/module.ts +18 -0
  48. package/src/client/pages/PageView.svelte +235 -5
  49. package/src/client/pages/TrashPage.svelte +378 -0
  50. package/src/client/query.ts +24 -0
  51. package/src/contract/models.ts +83 -0
  52. package/src/contract/permissions.ts +36 -0
  53. package/src/contract/router.ts +153 -1
@@ -1,13 +1,17 @@
1
- import { baseContract, Id, PageInput, page, WorkspaceId } from '@kernhq/contracts'
1
+ import { baseContract, Id, PageInput, page, UserId, WorkspaceId } from '@kernhq/contracts'
2
2
  import { z } from 'zod'
3
3
  import {
4
4
  CommentAnchor,
5
5
  CommentThread,
6
+ Favorite,
7
+ Label,
8
+ LabelColour,
6
9
  Ok,
7
10
  Page,
8
11
  PageKind,
9
12
  PageNode,
10
13
  PageVersion,
14
+ RecentView,
11
15
  RichDoc,
12
16
  Space,
13
17
  SpaceVisibility,
@@ -28,6 +32,41 @@ import {
28
32
  const ws = z.object({ workspaceId: WorkspaceId })
29
33
  const t = (...tags: string[]) => ({ tags })
30
34
 
35
+ /** The page fields a shortcut row draws, so a sidebar is one request rather than one per entry. */
36
+ const pageBits = {
37
+ spaceId: Id,
38
+ title: z.string(),
39
+ icon: z.string().nullable(),
40
+ kind: PageKind,
41
+ }
42
+
43
+ /**
44
+ * A favourite as a sidebar draws it: the shortcut, plus enough of its page to render the row.
45
+ *
46
+ * Composed here rather than stored, for the same reason `versions.get` composes its output here —
47
+ * it is the shape of one answer, not a second copy of the page. A favourite whose page has been
48
+ * trashed or purged is not in this list at all: the row survives (nothing cascades), and every read
49
+ * joins to `pages`, so it is simply never drawn.
50
+ */
51
+ export const FavoriteEntry = Favorite.extend(pageBits)
52
+ export type FavoriteEntry = z.infer<typeof FavoriteEntry>
53
+
54
+ /** The same, for a page somebody opened recently. */
55
+ export const RecentEntry = RecentView.extend(pageBits)
56
+ export type RecentEntry = z.infer<typeof RecentEntry>
57
+
58
+ /**
59
+ * Whether you are watching a page, and who else is.
60
+ *
61
+ * Both in one answer because a watch button has to draw both — its own pressed state and the number
62
+ * beside it — and asking twice within a keystroke of each other is two requests for one control.
63
+ */
64
+ export const WatchState = z.object({
65
+ watching: z.boolean(),
66
+ watchers: z.array(UserId),
67
+ })
68
+ export type WatchState = z.infer<typeof WatchState>
69
+
31
70
  export const quireContract = {
32
71
  spaces: {
33
72
  list: baseContract
@@ -137,6 +176,19 @@ export const quireContract = {
137
176
  .route({ method: 'DELETE', path: '/pages/{pageId}', ...t('pages') })
138
177
  .input(ws.extend({ pageId: Id }))
139
178
  .output(z.object({ ok: z.literal(true), count: z.number().int().nonnegative() })),
179
+ /**
180
+ * The labels on this page, replaced by exactly this set.
181
+ *
182
+ * `set`, not `add`: a picker with three labels ticked sends three, and a picker with one ticked
183
+ * sends one and means the other two are gone. An additive procedure cannot express unticking
184
+ * without a second one to pair with it, and the pair then has to agree about a page two people
185
+ * are editing. Every label has to be one of the space's own — a label belongs to a space, so
186
+ * putting another space's label on a page would leak its vocabulary across the boundary.
187
+ */
188
+ setLabels: baseContract
189
+ .route({ method: 'POST', path: '/pages/{pageId}/labels', ...t('pages') })
190
+ .input(ws.extend({ pageId: Id, labelIds: z.array(Id).max(50) }))
191
+ .output(z.array(Label)),
140
192
  },
141
193
 
142
194
  versions: {
@@ -355,6 +407,106 @@ export const quireContract = {
355
407
  .output(Ok),
356
408
  },
357
409
 
410
+ /**
411
+ * The vocabulary a space puts on its pages.
412
+ *
413
+ * Reading is `quire.space.view` and writing is `quire.space.manage`, because a label is part of a
414
+ * space's configuration rather than a page's content: renaming "Draft" changes what it means on
415
+ * every page that wears it, which is not a thing somebody who may edit one page should be able to
416
+ * do to everybody else's.
417
+ */
418
+ labels: {
419
+ list: baseContract
420
+ .route({ method: 'GET', path: '/spaces/{spaceId}/labels', ...t('labels') })
421
+ .input(ws.extend({ spaceId: Id }))
422
+ .output(z.array(Label)),
423
+ /** What one page wears. `pages.setLabels` is the other half; without this one nothing draws it. */
424
+ forPage: baseContract
425
+ .route({ method: 'GET', path: '/pages/{pageId}/labels', ...t('labels') })
426
+ .input(ws.extend({ pageId: Id }))
427
+ .output(z.array(Label)),
428
+ create: baseContract
429
+ .route({ method: 'POST', path: '/spaces/{spaceId}/labels', ...t('labels') })
430
+ .input(ws.extend({ spaceId: Id, name: Label.shape.name, colour: LabelColour.default('grey') }))
431
+ .output(Label),
432
+ update: baseContract
433
+ .route({ method: 'PATCH', path: '/labels/{labelId}', ...t('labels') })
434
+ .input(
435
+ ws.extend({
436
+ labelId: Id,
437
+ name: Label.shape.name.optional(),
438
+ colour: LabelColour.optional(),
439
+ }),
440
+ )
441
+ .output(Label),
442
+ /** Off every page that wore it, too — a label nothing can name is not a label anybody can remove. */
443
+ remove: baseContract
444
+ .route({ method: 'DELETE', path: '/labels/{labelId}', ...t('labels') })
445
+ .input(ws.extend({ labelId: Id }))
446
+ .output(Ok),
447
+ },
448
+
449
+ /**
450
+ * One person's own shortcuts, in the order they arranged them.
451
+ *
452
+ * Every procedure here is filtered to the caller. Row-level security fences the *workspace*, which
453
+ * is the tenant boundary and not a privacy boundary — one colleague's favourites are as visible to
454
+ * the policy as your own — so the `user_id` in each of these queries is the only thing keeping a
455
+ * sidebar personal. The three mutations all answer with the whole ordered list rather than the one
456
+ * row they touched: a fractional-index reorder is only meaningful as an ordering, the list is one
457
+ * person's and therefore short, and it saves the sidebar a refetch to redraw itself.
458
+ */
459
+ favorites: {
460
+ list: baseContract
461
+ .route({ method: 'GET', path: '/favorites', ...t('favorites') })
462
+ .input(ws)
463
+ .output(z.array(FavoriteEntry)),
464
+ /** Lands at the end. Starring the same page twice is the same star, not an error. */
465
+ add: baseContract
466
+ .route({ method: 'POST', path: '/favorites', ...t('favorites') })
467
+ .input(ws.extend({ pageId: Id }))
468
+ .output(z.array(FavoriteEntry)),
469
+ remove: baseContract
470
+ .route({ method: 'DELETE', path: '/favorites/{pageId}', ...t('favorites') })
471
+ .input(ws.extend({ pageId: Id }))
472
+ .output(z.array(FavoriteEntry)),
473
+ /** `afterId` is the favourite to land behind; null means first. The rank is minted server-side. */
474
+ reorder: baseContract
475
+ .route({ method: 'POST', path: '/favorites/{pageId}/move', ...t('favorites') })
476
+ .input(ws.extend({ pageId: Id, afterId: Id.nullable().default(null) }))
477
+ .output(z.array(FavoriteEntry)),
478
+ },
479
+
480
+ /** Who asked to hear about a page. Deliberately not the same list as who bookmarked it. */
481
+ watchers: {
482
+ get: baseContract
483
+ .route({ method: 'GET', path: '/pages/{pageId}/watchers', ...t('watchers') })
484
+ .input(ws.extend({ pageId: Id }))
485
+ .output(WatchState),
486
+ set: baseContract
487
+ .route({ method: 'POST', path: '/pages/{pageId}/watchers', ...t('watchers') })
488
+ .input(ws.extend({ pageId: Id, watching: z.boolean().default(true) }))
489
+ .output(WatchState),
490
+ },
491
+
492
+ /**
493
+ * Where this person has just been, newest first — and only this person.
494
+ *
495
+ * `record` is what a page view calls; it bumps one row rather than appending to a log, so the
496
+ * table is bounded by pages-times-people instead of growing for ever to answer a question that
497
+ * only ever wants the most recent handful.
498
+ */
499
+ recents: {
500
+ list: baseContract
501
+ .route({ method: 'GET', path: '/recents', ...t('recents') })
502
+ .input(ws.extend({ limit: z.number().int().min(1).max(50).default(10) }))
503
+ .output(z.array(RecentEntry)),
504
+ record: baseContract
505
+ .route({ method: 'POST', path: '/recents', ...t('recents') })
506
+ .input(ws.extend({ pageId: Id }))
507
+ .output(Ok),
508
+ },
509
+
358
510
  publishing: {
359
511
  /** Make what is written now the version readers are served. Only meaningful for a `page`. */
360
512
  publish: baseContract