@kernhq/module-quire 0.2.0 → 0.3.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 (44) hide show
  1. package/dist/contract/events.d.ts +11 -0
  2. package/dist/contract/events.d.ts.map +1 -1
  3. package/dist/contract/events.js +2 -0
  4. package/dist/contract/events.js.map +1 -1
  5. package/dist/contract/models.d.ts +29 -0
  6. package/dist/contract/models.d.ts.map +1 -1
  7. package/dist/contract/models.js +19 -0
  8. package/dist/contract/models.js.map +1 -1
  9. package/dist/contract/permissions.d.ts +7 -0
  10. package/dist/contract/permissions.d.ts.map +1 -1
  11. package/dist/contract/permissions.js +8 -0
  12. package/dist/contract/permissions.js.map +1 -1
  13. package/dist/contract/router.d.ts +285 -0
  14. package/dist/contract/router.d.ts.map +1 -1
  15. package/dist/contract/router.js +37 -1
  16. package/dist/contract/router.js.map +1 -1
  17. package/dist/server/_impl.d.ts +319 -0
  18. package/dist/server/_impl.d.ts.map +1 -1
  19. package/dist/server/_impl.js +90 -16
  20. package/dist/server/_impl.js.map +1 -1
  21. package/dist/server/index.d.ts.map +1 -1
  22. package/dist/server/index.js +63 -3
  23. package/dist/server/index.js.map +1 -1
  24. package/dist/server/schema.d.ts +206 -1
  25. package/dist/server/schema.d.ts.map +1 -1
  26. package/dist/server/schema.js +38 -2
  27. package/dist/server/schema.js.map +1 -1
  28. package/dist/server/services/index.d.ts +3 -0
  29. package/dist/server/services/index.d.ts.map +1 -1
  30. package/dist/server/services/index.js +3 -0
  31. package/dist/server/services/index.js.map +1 -1
  32. package/dist/server/services/versions.d.ts +124 -0
  33. package/dist/server/services/versions.d.ts.map +1 -0
  34. package/dist/server/services/versions.js +182 -0
  35. package/dist/server/services/versions.js.map +1 -0
  36. package/migrations/0002_page_versions.sql +25 -0
  37. package/migrations/meta/0002_snapshot.json +491 -0
  38. package/migrations/meta/_journal.json +7 -0
  39. package/package.json +1 -1
  40. package/src/client/index.ts +18 -1
  41. package/src/contract/events.ts +5 -0
  42. package/src/contract/models.ts +23 -0
  43. package/src/contract/permissions.ts +8 -0
  44. package/src/contract/router.ts +39 -1
@@ -0,0 +1,491 @@
1
+ {
2
+ "id": "09606cee-18e2-41eb-b1a6-44a2bf9856cb",
3
+ "prevId": "73e5ffbb-8a14-4b41-bdfd-776e51bec7d7",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "mod_quire.page_versions": {
8
+ "name": "page_versions",
9
+ "schema": "mod_quire",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "uuid",
14
+ "primaryKey": true,
15
+ "notNull": true,
16
+ "default": "uuidv7()"
17
+ },
18
+ "workspace_id": {
19
+ "name": "workspace_id",
20
+ "type": "uuid",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
24
+ "page_id": {
25
+ "name": "page_id",
26
+ "type": "uuid",
27
+ "primaryKey": false,
28
+ "notNull": true
29
+ },
30
+ "kind": {
31
+ "name": "kind",
32
+ "type": "text",
33
+ "primaryKey": false,
34
+ "notNull": true,
35
+ "default": "'auto'"
36
+ },
37
+ "label": {
38
+ "name": "label",
39
+ "type": "text",
40
+ "primaryKey": false,
41
+ "notNull": false
42
+ },
43
+ "state": {
44
+ "name": "state",
45
+ "type": "bytea",
46
+ "primaryKey": false,
47
+ "notNull": true
48
+ },
49
+ "snapshot": {
50
+ "name": "snapshot",
51
+ "type": "bytea",
52
+ "primaryKey": false,
53
+ "notNull": false
54
+ },
55
+ "text": {
56
+ "name": "text",
57
+ "type": "text",
58
+ "primaryKey": false,
59
+ "notNull": true,
60
+ "default": "''"
61
+ },
62
+ "size": {
63
+ "name": "size",
64
+ "type": "integer",
65
+ "primaryKey": false,
66
+ "notNull": true,
67
+ "default": 0
68
+ },
69
+ "author_id": {
70
+ "name": "author_id",
71
+ "type": "uuid",
72
+ "primaryKey": false,
73
+ "notNull": false
74
+ },
75
+ "created_at": {
76
+ "name": "created_at",
77
+ "type": "timestamp with time zone",
78
+ "primaryKey": false,
79
+ "notNull": true,
80
+ "default": "now()"
81
+ }
82
+ },
83
+ "indexes": {
84
+ "page_versions_ws_page_idx": {
85
+ "name": "page_versions_ws_page_idx",
86
+ "columns": [
87
+ {
88
+ "expression": "workspace_id",
89
+ "isExpression": false,
90
+ "asc": true,
91
+ "nulls": "last"
92
+ },
93
+ {
94
+ "expression": "page_id",
95
+ "isExpression": false,
96
+ "asc": true,
97
+ "nulls": "last"
98
+ },
99
+ {
100
+ "expression": "created_at",
101
+ "isExpression": false,
102
+ "asc": true,
103
+ "nulls": "last"
104
+ }
105
+ ],
106
+ "isUnique": false,
107
+ "concurrently": false,
108
+ "method": "btree",
109
+ "with": {}
110
+ },
111
+ "page_versions_ws_created_idx": {
112
+ "name": "page_versions_ws_created_idx",
113
+ "columns": [
114
+ {
115
+ "expression": "workspace_id",
116
+ "isExpression": false,
117
+ "asc": true,
118
+ "nulls": "last"
119
+ },
120
+ {
121
+ "expression": "created_at",
122
+ "isExpression": false,
123
+ "asc": true,
124
+ "nulls": "last"
125
+ }
126
+ ],
127
+ "isUnique": false,
128
+ "concurrently": false,
129
+ "method": "btree",
130
+ "with": {}
131
+ }
132
+ },
133
+ "foreignKeys": {},
134
+ "compositePrimaryKeys": {},
135
+ "uniqueConstraints": {},
136
+ "policies": {},
137
+ "checkConstraints": {},
138
+ "isRLSEnabled": false
139
+ },
140
+ "mod_quire.pages": {
141
+ "name": "pages",
142
+ "schema": "mod_quire",
143
+ "columns": {
144
+ "id": {
145
+ "name": "id",
146
+ "type": "uuid",
147
+ "primaryKey": true,
148
+ "notNull": true,
149
+ "default": "uuidv7()"
150
+ },
151
+ "workspace_id": {
152
+ "name": "workspace_id",
153
+ "type": "uuid",
154
+ "primaryKey": false,
155
+ "notNull": true
156
+ },
157
+ "space_id": {
158
+ "name": "space_id",
159
+ "type": "uuid",
160
+ "primaryKey": false,
161
+ "notNull": true
162
+ },
163
+ "parent_id": {
164
+ "name": "parent_id",
165
+ "type": "uuid",
166
+ "primaryKey": false,
167
+ "notNull": false
168
+ },
169
+ "position": {
170
+ "name": "position",
171
+ "type": "text",
172
+ "primaryKey": false,
173
+ "notNull": true
174
+ },
175
+ "kind": {
176
+ "name": "kind",
177
+ "type": "text",
178
+ "primaryKey": false,
179
+ "notNull": true,
180
+ "default": "'page'"
181
+ },
182
+ "title": {
183
+ "name": "title",
184
+ "type": "text",
185
+ "primaryKey": false,
186
+ "notNull": true,
187
+ "default": "''"
188
+ },
189
+ "icon": {
190
+ "name": "icon",
191
+ "type": "text",
192
+ "primaryKey": false,
193
+ "notNull": false
194
+ },
195
+ "cover_url": {
196
+ "name": "cover_url",
197
+ "type": "text",
198
+ "primaryKey": false,
199
+ "notNull": false
200
+ },
201
+ "published_version_id": {
202
+ "name": "published_version_id",
203
+ "type": "uuid",
204
+ "primaryKey": false,
205
+ "notNull": false
206
+ },
207
+ "has_unpublished_changes": {
208
+ "name": "has_unpublished_changes",
209
+ "type": "boolean",
210
+ "primaryKey": false,
211
+ "notNull": true,
212
+ "default": false
213
+ },
214
+ "text": {
215
+ "name": "text",
216
+ "type": "text",
217
+ "primaryKey": false,
218
+ "notNull": true,
219
+ "default": "''"
220
+ },
221
+ "created_by": {
222
+ "name": "created_by",
223
+ "type": "uuid",
224
+ "primaryKey": false,
225
+ "notNull": false
226
+ },
227
+ "updated_by": {
228
+ "name": "updated_by",
229
+ "type": "uuid",
230
+ "primaryKey": false,
231
+ "notNull": false
232
+ },
233
+ "created_at": {
234
+ "name": "created_at",
235
+ "type": "timestamp with time zone",
236
+ "primaryKey": false,
237
+ "notNull": true,
238
+ "default": "now()"
239
+ },
240
+ "updated_at": {
241
+ "name": "updated_at",
242
+ "type": "timestamp with time zone",
243
+ "primaryKey": false,
244
+ "notNull": true,
245
+ "default": "now()"
246
+ },
247
+ "archived_at": {
248
+ "name": "archived_at",
249
+ "type": "timestamp with time zone",
250
+ "primaryKey": false,
251
+ "notNull": false
252
+ },
253
+ "deleted_at": {
254
+ "name": "deleted_at",
255
+ "type": "timestamp with time zone",
256
+ "primaryKey": false,
257
+ "notNull": false
258
+ }
259
+ },
260
+ "indexes": {
261
+ "pages_ws_space_idx": {
262
+ "name": "pages_ws_space_idx",
263
+ "columns": [
264
+ {
265
+ "expression": "workspace_id",
266
+ "isExpression": false,
267
+ "asc": true,
268
+ "nulls": "last"
269
+ },
270
+ {
271
+ "expression": "space_id",
272
+ "isExpression": false,
273
+ "asc": true,
274
+ "nulls": "last"
275
+ },
276
+ {
277
+ "expression": "position",
278
+ "isExpression": false,
279
+ "asc": true,
280
+ "nulls": "last"
281
+ }
282
+ ],
283
+ "isUnique": false,
284
+ "concurrently": false,
285
+ "method": "btree",
286
+ "with": {}
287
+ },
288
+ "pages_ws_parent_idx": {
289
+ "name": "pages_ws_parent_idx",
290
+ "columns": [
291
+ {
292
+ "expression": "workspace_id",
293
+ "isExpression": false,
294
+ "asc": true,
295
+ "nulls": "last"
296
+ },
297
+ {
298
+ "expression": "parent_id",
299
+ "isExpression": false,
300
+ "asc": true,
301
+ "nulls": "last"
302
+ },
303
+ {
304
+ "expression": "position",
305
+ "isExpression": false,
306
+ "asc": true,
307
+ "nulls": "last"
308
+ }
309
+ ],
310
+ "isUnique": false,
311
+ "concurrently": false,
312
+ "method": "btree",
313
+ "with": {}
314
+ },
315
+ "pages_ws_updated_idx": {
316
+ "name": "pages_ws_updated_idx",
317
+ "columns": [
318
+ {
319
+ "expression": "workspace_id",
320
+ "isExpression": false,
321
+ "asc": true,
322
+ "nulls": "last"
323
+ },
324
+ {
325
+ "expression": "updated_at",
326
+ "isExpression": false,
327
+ "asc": true,
328
+ "nulls": "last"
329
+ }
330
+ ],
331
+ "isUnique": false,
332
+ "concurrently": false,
333
+ "method": "btree",
334
+ "with": {}
335
+ }
336
+ },
337
+ "foreignKeys": {},
338
+ "compositePrimaryKeys": {},
339
+ "uniqueConstraints": {},
340
+ "policies": {},
341
+ "checkConstraints": {},
342
+ "isRLSEnabled": false
343
+ },
344
+ "mod_quire.spaces": {
345
+ "name": "spaces",
346
+ "schema": "mod_quire",
347
+ "columns": {
348
+ "id": {
349
+ "name": "id",
350
+ "type": "uuid",
351
+ "primaryKey": true,
352
+ "notNull": true,
353
+ "default": "uuidv7()"
354
+ },
355
+ "workspace_id": {
356
+ "name": "workspace_id",
357
+ "type": "uuid",
358
+ "primaryKey": false,
359
+ "notNull": true
360
+ },
361
+ "key": {
362
+ "name": "key",
363
+ "type": "text",
364
+ "primaryKey": false,
365
+ "notNull": true
366
+ },
367
+ "name": {
368
+ "name": "name",
369
+ "type": "text",
370
+ "primaryKey": false,
371
+ "notNull": true
372
+ },
373
+ "description": {
374
+ "name": "description",
375
+ "type": "text",
376
+ "primaryKey": false,
377
+ "notNull": true,
378
+ "default": "''"
379
+ },
380
+ "icon": {
381
+ "name": "icon",
382
+ "type": "text",
383
+ "primaryKey": false,
384
+ "notNull": false
385
+ },
386
+ "visibility": {
387
+ "name": "visibility",
388
+ "type": "text",
389
+ "primaryKey": false,
390
+ "notNull": true,
391
+ "default": "'open'"
392
+ },
393
+ "homepage_id": {
394
+ "name": "homepage_id",
395
+ "type": "uuid",
396
+ "primaryKey": false,
397
+ "notNull": false
398
+ },
399
+ "created_by": {
400
+ "name": "created_by",
401
+ "type": "uuid",
402
+ "primaryKey": false,
403
+ "notNull": false
404
+ },
405
+ "created_at": {
406
+ "name": "created_at",
407
+ "type": "timestamp with time zone",
408
+ "primaryKey": false,
409
+ "notNull": true,
410
+ "default": "now()"
411
+ },
412
+ "updated_at": {
413
+ "name": "updated_at",
414
+ "type": "timestamp with time zone",
415
+ "primaryKey": false,
416
+ "notNull": true,
417
+ "default": "now()"
418
+ },
419
+ "archived_at": {
420
+ "name": "archived_at",
421
+ "type": "timestamp with time zone",
422
+ "primaryKey": false,
423
+ "notNull": false
424
+ }
425
+ },
426
+ "indexes": {
427
+ "spaces_ws_key_uq": {
428
+ "name": "spaces_ws_key_uq",
429
+ "columns": [
430
+ {
431
+ "expression": "workspace_id",
432
+ "isExpression": false,
433
+ "asc": true,
434
+ "nulls": "last"
435
+ },
436
+ {
437
+ "expression": "key",
438
+ "isExpression": false,
439
+ "asc": true,
440
+ "nulls": "last"
441
+ }
442
+ ],
443
+ "isUnique": true,
444
+ "concurrently": false,
445
+ "method": "btree",
446
+ "with": {}
447
+ },
448
+ "spaces_ws_idx": {
449
+ "name": "spaces_ws_idx",
450
+ "columns": [
451
+ {
452
+ "expression": "workspace_id",
453
+ "isExpression": false,
454
+ "asc": true,
455
+ "nulls": "last"
456
+ },
457
+ {
458
+ "expression": "created_at",
459
+ "isExpression": false,
460
+ "asc": true,
461
+ "nulls": "last"
462
+ }
463
+ ],
464
+ "isUnique": false,
465
+ "concurrently": false,
466
+ "method": "btree",
467
+ "with": {}
468
+ }
469
+ },
470
+ "foreignKeys": {},
471
+ "compositePrimaryKeys": {},
472
+ "uniqueConstraints": {},
473
+ "policies": {},
474
+ "checkConstraints": {},
475
+ "isRLSEnabled": false
476
+ }
477
+ },
478
+ "enums": {},
479
+ "schemas": {
480
+ "mod_quire": "mod_quire"
481
+ },
482
+ "sequences": {},
483
+ "roles": {},
484
+ "policies": {},
485
+ "views": {},
486
+ "_meta": {
487
+ "columns": {},
488
+ "schemas": {},
489
+ "tables": {}
490
+ }
491
+ }
@@ -15,6 +15,13 @@
15
15
  "when": 1787566824449,
16
16
  "tag": "0001_rls",
17
17
  "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "7",
22
+ "when": 1787572772745,
23
+ "tag": "0002_page_versions",
24
+ "breakpoints": true
18
25
  }
19
26
  ]
20
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kernhq/module-quire",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Kern Quire: collaborative documents, spaces and page trees",
5
5
  "homepage": "https://kernaio.com",
6
6
  "license": "AGPL-3.0-only",
@@ -1,6 +1,7 @@
1
+ import { formatCollabDocument } from '@kernhq/contracts'
1
2
  import { createModuleClient, type KernClientOptions } from '@kernhq/sdk'
2
3
  import type { ContractRouterClient } from '@orpc/contract'
3
- import type { PageNode, QuireContract } from '../contract/index.js'
4
+ import { MODULE_ID, type Page, type PageNode, type QuireContract } from '../contract/index.js'
4
5
 
5
6
  /**
6
7
  * The client half.
@@ -33,6 +34,22 @@ export {
33
34
  } from '../contract/index.js'
34
35
  export * from './rank.js'
35
36
 
37
+ /**
38
+ * The name this page's prose is synchronised under, on the collab service.
39
+ *
40
+ * Exported here rather than left to the caller because the gateway parses it with the matching
41
+ * function from `@kernhq/contracts`, and a name it cannot parse is a rejected connection with no
42
+ * useful error. The module owns the naming of its own objects.
43
+ */
44
+ export function pageDocumentName(page: { workspaceId: Page['workspaceId']; id: string }): string {
45
+ return formatCollabDocument({
46
+ workspaceId: page.workspaceId,
47
+ module: MODULE_ID,
48
+ type: 'page',
49
+ objectId: page.id,
50
+ })
51
+ }
52
+
36
53
  /** The permission keys, so the app gates on a constant rather than a string it retyped. */
37
54
  export const QUIRE_PERMISSIONS = {
38
55
  spaceView: 'quire.space.view',
@@ -17,6 +17,11 @@ export const quireEvents = {
17
17
  pageArchived: defineEvent('quire.page.archived', page.extend({ archived: z.boolean() })),
18
18
  pageTrashed: defineEvent('quire.page.trashed', page.extend({ count: z.number().int().nonnegative() })),
19
19
  pageRestored: defineEvent('quire.page.restored', page),
20
+ pagePublished: defineEvent('quire.page.published', page.extend({ versionId: Id })),
21
+ pageRestoredVersion: defineEvent(
22
+ 'quire.page.restored_version',
23
+ z.object({ pageId: Id, workspaceId: WorkspaceId, versionId: Id }),
24
+ ),
20
25
  /** The page and its descendants are gone; anything holding a reference should drop it. */
21
26
  pageDeleted: defineEvent('quire.page.deleted', page.extend({ pageIds: z.array(Id) })),
22
27
  } as const
@@ -90,4 +90,27 @@ export const PageNode = z.object({
90
90
  })
91
91
  export type PageNode = z.infer<typeof PageNode>
92
92
 
93
+ /**
94
+ * How a version came to exist. `restore` matters: putting an older version back writes a new
95
+ * version rather than rewinding, so restoring is never itself the thing that loses work.
96
+ */
97
+ export const VersionKind = z.enum(['auto', 'publish', 'restore', 'import'])
98
+ export type VersionKind = z.infer<typeof VersionKind>
99
+
100
+ export const PageVersion = z.object({
101
+ id: Id,
102
+ workspaceId: WorkspaceId,
103
+ pageId: Id,
104
+ kind: VersionKind,
105
+ label: z.string().max(120).nullable(),
106
+ /** the first line or so of the prose, so a list reads without loading a document */
107
+ preview: z.string(),
108
+ size: z.number().int().nonnegative(),
109
+ authorId: UserId.nullable(),
110
+ createdAt: Timestamp,
111
+ /** whether this is the version a reader is currently served */
112
+ published: z.boolean(),
113
+ })
114
+ export type PageVersion = z.infer<typeof PageVersion>
115
+
93
116
  export const Ok = z.object({ ok: z.literal(true) })
@@ -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.publish',
53
+ label: 'Publish pages',
54
+ description: 'Decide which version readers and any public site are served',
55
+ scope: 'space',
56
+ defaultRoles: ['owner', 'admin', 'member'],
57
+ dangerous: false,
58
+ },
51
59
  {
52
60
  key: 'quire.page.delete',
53
61
  label: 'Delete pages permanently',
@@ -1,6 +1,6 @@
1
1
  import { baseContract, Id, PageInput, page, WorkspaceId } from '@kernhq/contracts'
2
2
  import { z } from 'zod'
3
- import { Ok, Page, PageKind, PageNode, Space, SpaceVisibility } from './models.js'
3
+ import { Ok, Page, PageKind, PageNode, PageVersion, Space, SpaceVisibility } from './models.js'
4
4
 
5
5
  const ws = z.object({ workspaceId: WorkspaceId })
6
6
  const t = (...tags: string[]) => ({ tags })
@@ -115,6 +115,44 @@ export const quireContract = {
115
115
  .input(ws.extend({ pageId: Id }))
116
116
  .output(z.object({ ok: z.literal(true), count: z.number().int().nonnegative() })),
117
117
  },
118
+
119
+ versions: {
120
+ list: baseContract
121
+ .route({ method: 'GET', path: '/pages/{pageId}/versions', ...t('versions') })
122
+ .input(ws.extend({ pageId: Id }).extend(PageInput.shape))
123
+ .output(page(PageVersion)),
124
+ /** The prose of one version, rendered as text — enough to read it and to diff it. */
125
+ get: baseContract
126
+ .route({ method: 'GET', path: '/versions/{versionId}', ...t('versions') })
127
+ .input(ws.extend({ versionId: Id }))
128
+ .output(PageVersion.extend({ text: z.string() })),
129
+ /** Take one now, and give it a name. */
130
+ create: baseContract
131
+ .route({ method: 'POST', path: '/pages/{pageId}/versions', ...t('versions') })
132
+ .input(ws.extend({ pageId: Id, label: z.string().min(1).max(120).nullable().default(null) }))
133
+ .output(PageVersion),
134
+ /**
135
+ * Put an older version back. It is applied to the live document rather than written behind the
136
+ * people editing it, and the state it replaced is kept as a version of its own first.
137
+ */
138
+ restore: baseContract
139
+ .route({ method: 'POST', path: '/versions/{versionId}/restore', ...t('versions') })
140
+ .input(ws.extend({ versionId: Id }))
141
+ .output(PageVersion),
142
+ },
143
+
144
+ publishing: {
145
+ /** Make what is written now the version readers are served. Only meaningful for a `page`. */
146
+ publish: baseContract
147
+ .route({ method: 'POST', path: '/pages/{pageId}/publish', ...t('publishing') })
148
+ .input(ws.extend({ pageId: Id, label: z.string().min(1).max(120).nullable().default(null) }))
149
+ .output(Page),
150
+ /** Throw the draft away and go back to what readers can already see. */
151
+ revert: baseContract
152
+ .route({ method: 'POST', path: '/pages/{pageId}/revert', ...t('publishing') })
153
+ .input(ws.extend({ pageId: Id }))
154
+ .output(Page),
155
+ },
118
156
  } as const
119
157
  export type QuireContract = typeof quireContract
120
158