@kernhq/module-quire 0.14.0 → 0.16.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 (74) hide show
  1. package/dist/contract/models.d.ts +209 -0
  2. package/dist/contract/models.d.ts.map +1 -1
  3. package/dist/contract/models.js +151 -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/properties.d.ts +3 -3
  9. package/dist/contract/router.d.ts +509 -8
  10. package/dist/contract/router.d.ts.map +1 -1
  11. package/dist/contract/router.js +211 -1
  12. package/dist/contract/router.js.map +1 -1
  13. package/dist/server/_impl.d.ts +573 -1269
  14. package/dist/server/_impl.d.ts.map +1 -1
  15. package/dist/server/_impl.js +180 -0
  16. package/dist/server/_impl.js.map +1 -1
  17. package/dist/server/export/markdown.d.ts.map +1 -1
  18. package/dist/server/export/markdown.js +84 -1
  19. package/dist/server/export/markdown.js.map +1 -1
  20. package/dist/server/render.d.ts +190 -1
  21. package/dist/server/render.d.ts.map +1 -1
  22. package/dist/server/render.js +413 -0
  23. package/dist/server/render.js.map +1 -1
  24. package/dist/server/schema.d.ts +260 -1
  25. package/dist/server/schema.d.ts.map +1 -1
  26. package/dist/server/schema.js +108 -1
  27. package/dist/server/schema.js.map +1 -1
  28. package/dist/server/services/databases.d.ts +5 -5
  29. package/dist/server/services/index.d.ts +6 -0
  30. package/dist/server/services/index.d.ts.map +1 -1
  31. package/dist/server/services/index.js +27 -3
  32. package/dist/server/services/index.js.map +1 -1
  33. package/dist/server/services/macros.d.ts +83 -0
  34. package/dist/server/services/macros.d.ts.map +1 -0
  35. package/dist/server/services/macros.js +488 -0
  36. package/dist/server/services/macros.js.map +1 -0
  37. package/dist/server/services/objects.d.ts +61 -0
  38. package/dist/server/services/objects.d.ts.map +1 -0
  39. package/dist/server/services/objects.js +110 -0
  40. package/dist/server/services/objects.js.map +1 -0
  41. package/dist/server/services/publications.d.ts +2 -1
  42. package/dist/server/services/publications.d.ts.map +1 -1
  43. package/dist/server/services/publications.js +43 -4
  44. package/dist/server/services/publications.js.map +1 -1
  45. package/dist/server/services/templates.d.ts +135 -0
  46. package/dist/server/services/templates.d.ts.map +1 -0
  47. package/dist/server/services/templates.js +897 -0
  48. package/dist/server/services/templates.js.map +1 -0
  49. package/dist/server/services/unfurl.d.ts +154 -0
  50. package/dist/server/services/unfurl.d.ts.map +1 -0
  51. package/dist/server/services/unfurl.js +593 -0
  52. package/dist/server/services/unfurl.js.map +1 -0
  53. package/dist/server/services/versions.d.ts +12 -0
  54. package/dist/server/services/versions.d.ts.map +1 -1
  55. package/dist/server/services/versions.js +3 -1
  56. package/dist/server/services/versions.js.map +1 -1
  57. package/migrations/0011_templates.sql +157 -0
  58. package/migrations/meta/_journal.json +7 -0
  59. package/package.json +5 -5
  60. package/src/client/components/NewSpaceDialog.svelte +77 -8
  61. package/src/client/components/PageEditor.svelte +80 -0
  62. package/src/client/components/PagePicker.svelte +264 -0
  63. package/src/client/components/SaveAsTemplateDialog.svelte +502 -0
  64. package/src/client/components/SidebarSpaces.svelte +33 -1
  65. package/src/client/components/TemplatePicker.svelte +437 -0
  66. package/src/client/i18n.ts +327 -0
  67. package/src/client/index.ts +19 -0
  68. package/src/client/mock.ts +274 -0
  69. package/src/client/pages/PageView.svelte +50 -0
  70. package/src/client/pages/SpacePage.svelte +20 -4
  71. package/src/client/query.ts +17 -0
  72. package/src/contract/models.ts +191 -0
  73. package/src/contract/permissions.ts +33 -0
  74. package/src/contract/router.ts +228 -0
@@ -289,6 +289,39 @@ export const quireProcedureAuthz: Record<string, ProcedureAuthz> = {
289
289
  'imports.get': { check: 'workspace', permission: 'quire.page.import' },
290
290
  'imports.list': { check: 'workspace', permission: 'quire.page.import' },
291
291
 
292
+ /*
293
+ * A template is a space's furniture, so it follows `labels.*` exactly: reading is
294
+ * `quire.space.view` and writing is `quire.space.manage`. Changing one changes what everybody in
295
+ * the space is offered the next time they make a page, which is not a thing somebody who may edit
296
+ * one page should be able to do to everybody else's.
297
+ *
298
+ * Three of the five are `check: 'space'` although four of them take a *template* id, not a space
299
+ * id. A template id carries no scope of its own, so the row is read first and the question is
300
+ * asked about the space it belongs to — the same shape as `labels.update` next door. A
301
+ * workspace-wide template (`space_id is null`) has no narrower scope than the workspace, and the
302
+ * handlers say so out loud rather than silently falling back to the space of whoever asked.
303
+ *
304
+ * `createFromPage` is the one that is `check: 'page'`, and it is the interesting one: it *copies a
305
+ * page's prose into a template*, so the question that actually protects anything is whether this
306
+ * person may read that page. A page-scoped DENY of `quire.page.view` has to refuse it, or a
307
+ * contractor allowed one page of a handbook could lift a page they cannot open into a template and
308
+ * read it there. `quire.space.manage` is asked as well, in the same handler, and
309
+ * `templates.int.test.ts` covers that half — this column names one permission, and the one worth
310
+ * proving with a page-scoped binding is the read.
311
+ *
312
+ * `instantiate` asks `quire.page.create` and not `space.manage`: what it does is make a page.
313
+ * Somebody who may write in a space may use its templates; only somebody who configures the space
314
+ * may change them. Its `space` branch — which makes a *space* — asks `quire.space.manage` at
315
+ * workspace scope on top, for the same reason `spaces.create` does; the sweep sends the page
316
+ * branch, and `templates.int.test.ts` covers the other.
317
+ */
318
+ 'templates.list': { check: 'space', permission: 'quire.space.view' },
319
+ 'templates.get': { check: 'space', permission: 'quire.space.view' },
320
+ 'templates.createFromPage': { check: 'page', permission: 'quire.page.view' },
321
+ 'templates.update': { check: 'space', permission: 'quire.space.manage' },
322
+ 'templates.remove': { check: 'space', permission: 'quire.space.manage' },
323
+ 'templates.instantiate': { check: 'space', permission: 'quire.page.create' },
324
+
292
325
  'publishing.publish': { check: 'page', permission: 'quire.page.publish' },
293
326
  'publishing.revert': { check: 'page', permission: 'quire.page.edit' },
294
327
 
@@ -22,6 +22,10 @@ import {
22
22
  RichDoc,
23
23
  Space,
24
24
  SpaceVisibility,
25
+ Template,
26
+ TemplateKind,
27
+ TemplateStarterKey,
28
+ TemplateVariable,
25
29
  } from './models.js'
26
30
  import {
27
31
  Database,
@@ -287,6 +291,77 @@ export const PublicBasePath = z
287
291
  */
288
292
  export const PUBLIC_ASSET_SEGMENT = '__media'
289
293
 
294
+ /**
295
+ * One row of the picker: a shipped starter, or a template somebody saved.
296
+ *
297
+ * The two are one list on purpose. A person choosing what to write does not care which of them came
298
+ * with Kern, so a screen that draws "Templates" and "Your templates" as separate sections is asking
299
+ * them to know something about our packaging — and it makes the override rule invisible, because a
300
+ * starter a workspace has edited would appear in the second list while its shipped twin sat in the
301
+ * first.
302
+ *
303
+ * **`id` is null for a shipped starter, and that is the whole shape of the compromise.** A starter is
304
+ * a constant in the module rather than a row in the customer's database — `migrations/0011` argues
305
+ * why at length — so there is no row for it to have an id, and `key` is what addresses it instead.
306
+ * `instantiate` therefore takes one of the two and refuses both.
307
+ *
308
+ * `name` and `description` arrive **already in the reader's language**. A starter's strings are a
309
+ * table in the module resolved against `principal.locale`, so a client renders whatever it is given
310
+ * and never has to know which entries are ours.
311
+ */
312
+ export const TemplateChoice = z.object({
313
+ /** null for a shipped starter — a constant has no row, so it has no id */
314
+ id: Id.nullable(),
315
+ /**
316
+ * The starter this entry is, or replaces; null for somebody's own template.
317
+ *
318
+ * A row carrying a key **stands in for** that starter rather than appearing beside it, so this
319
+ * list never contains two entries with the same key.
320
+ */
321
+ key: z.string().nullable(),
322
+ builtIn: z.boolean(),
323
+ kind: TemplateKind,
324
+ spaceId: Id.nullable(),
325
+ name: z.string(),
326
+ description: z.string(),
327
+ icon: z.string().nullable(),
328
+ variables: z.array(TemplateVariable),
329
+ /** null for a shipped starter nobody has edited — it has no row and therefore no history */
330
+ updatedAt: Timestamp.nullable(),
331
+ })
332
+ export type TemplateChoice = z.infer<typeof TemplateChoice>
333
+
334
+ /**
335
+ * What somebody typed into the form a template asked for.
336
+ *
337
+ * Strings whatever the variable's type, because substitution is textual: a `date` variable puts
338
+ * characters into a paragraph exactly as a `text` one does, and the type only ever decided which
339
+ * control the person was shown. Storing a number here would mean the server deciding how to format
340
+ * it, in a locale it would have to guess.
341
+ *
342
+ * A key naming no declared variable is ignored rather than refused — a client one release ahead is
343
+ * a normal thing during a rolling deploy, and refusing the whole page over a spare field is not.
344
+ */
345
+ export const TemplateValues = z.record(z.string().max(40), z.string().max(2000))
346
+
347
+ /**
348
+ * What making something from a template produced.
349
+ *
350
+ * One shape for both kinds, rather than `Page` for one and `Space` for the other. The caller's next
351
+ * move is the same either way — open what was just made — and a union output would make every
352
+ * client branch on `kind` to find out where to navigate. `pageCount` is what the screen reports:
353
+ * a space template that made eleven pages should say so, because that is a lot of pages to have
354
+ * appeared in a sidebar without warning.
355
+ */
356
+ export const TemplateResult = z.object({
357
+ /** the new space, for a space template; the space the new page landed in otherwise */
358
+ spaceId: Id,
359
+ /** what to open: the new page, or the first page of the new space's tree. Null for an empty tree. */
360
+ pageId: Id.nullable(),
361
+ pageCount: z.number().int().nonnegative(),
362
+ })
363
+ export type TemplateResult = z.infer<typeof TemplateResult>
364
+
290
365
  export const quireContract = {
291
366
  spaces: {
292
367
  list: baseContract
@@ -727,6 +802,159 @@ export const quireContract = {
727
802
  .output(Ok),
728
803
  },
729
804
 
805
+ /**
806
+ * What somebody writes with: a page, or a whole space, saved so it can be made again.
807
+ *
808
+ * **The five starters Kern ships are constants in this module, not rows in a customer's
809
+ * database.** `migrations/0011_templates.sql` argues that at length; the consequences visible from
810
+ * here are three. `list` answers starters and rows in one list, with a row carrying a starter's
811
+ * `key` standing *in place of* that starter rather than beside it. A starter has no id, so
812
+ * `instantiate` takes `templateId` **or** `starterKey` and refuses both. And `get`, `update` and
813
+ * `remove` take an id, which means they are about rows only — a starter has nothing to fetch or
814
+ * delete, and "reset this one" is deleting the row that replaced it.
815
+ *
816
+ * Reading is `quire.space.view` and writing is `quire.space.manage`, exactly as `labels.*` next
817
+ * door, and for the same reason: a template is part of a space's furniture rather than one page's
818
+ * content. Changing it changes what everybody in the space is offered when they make a page, which
819
+ * is not a thing somebody who may edit one page should be able to do to everybody else's.
820
+ *
821
+ * `instantiate` is the exception and asks `quire.page.create`, because what it does is make a
822
+ * page. Somebody who may write in a space may use its templates; only somebody who configures the
823
+ * space may change them.
824
+ */
825
+ templates: {
826
+ /**
827
+ * What may be made here: the starters, plus this workspace's own, with overrides applied.
828
+ *
829
+ * `spaceId` is the space being written into, and null asks the workspace-wide question — which
830
+ * is what the "New space" picker needs, because there is no space yet. With a space named, the
831
+ * answer is that space's templates *and* the workspace-wide ones: a template scoped to a space
832
+ * is an addition to what is offered there, never a replacement for what is offered everywhere.
833
+ *
834
+ * No body comes back. Thirty page documents to draw thirty names is thirty documents nobody
835
+ * reads — see `TemplateSummary`. `instantiate` is what reads one.
836
+ */
837
+ list: baseContract
838
+ .route({ method: 'GET', path: '/templates', ...t('templates') })
839
+ .input(
840
+ ws.extend({
841
+ kind: TemplateKind.default('page'),
842
+ /** the space a page would be made in; null asks only what is offered everywhere */
843
+ spaceId: Id.nullable().default(null),
844
+ }),
845
+ )
846
+ .output(z.array(TemplateChoice)),
847
+ /**
848
+ * One saved template, body and all — what the edit screen loads.
849
+ *
850
+ * Rows only. A starter has no row, and inventing an id for it would make every other procedure
851
+ * here have to tell the two kinds of id apart.
852
+ */
853
+ get: baseContract
854
+ .route({ method: 'GET', path: '/templates/{templateId}', ...t('templates') })
855
+ .input(ws.extend({ templateId: Id }))
856
+ .output(Template),
857
+ /**
858
+ * Save what is written now as a template.
859
+ *
860
+ * `sourceId` names the page for `kind: 'page'` and the **space** for `kind: 'space'`, the same
861
+ * shape as `exports.start`'s `targetId` and for the same reason: the two kinds have nothing in
862
+ * common to point at, and two nullable id fields would let a caller send both. The procedure is
863
+ * named for the common case; a space template reads the space's whole tree.
864
+ *
865
+ * `key` is what makes a starter editable at all. Passing one writes a row that **replaces** that
866
+ * starter in this workspace's picker; deleting the row brings the shipped one back, current and
867
+ * translated. One row per starter per workspace — the second is a conflict, not a second entry.
868
+ */
869
+ createFromPage: baseContract
870
+ .route({ method: 'POST', path: '/templates', ...t('templates') })
871
+ .input(
872
+ ws.extend({
873
+ kind: TemplateKind.default('page'),
874
+ /** the page for `page`; the space for `space` — `kind` says which */
875
+ sourceId: Id,
876
+ /** null offers it everywhere in the workspace; a space id scopes it to that space */
877
+ spaceId: Id.nullable().default(null),
878
+ name: z.string().min(1).max(120),
879
+ description: z.string().max(2000).default(''),
880
+ icon: z.string().max(64).nullable().default(null),
881
+ variables: z.array(TemplateVariable).max(25).default([]),
882
+ /** replace this shipped starter rather than sitting beside it */
883
+ key: TemplateStarterKey.nullable().default(null),
884
+ }),
885
+ )
886
+ .output(Template),
887
+ /**
888
+ * Rename it, re-scope it, change what it asks for — or replace its body with a page's.
889
+ *
890
+ * `sourceId` is three-valued like `publications.update`'s password: a page (or space) id takes
891
+ * the body from there again, and leaving the key out changes nothing. Without that, updating a
892
+ * template's name would be a separate act from updating its prose, and the second one would have
893
+ * no procedure at all.
894
+ */
895
+ update: baseContract
896
+ .route({ method: 'PATCH', path: '/templates/{templateId}', ...t('templates') })
897
+ .input(
898
+ ws.extend({
899
+ templateId: Id,
900
+ name: z.string().min(1).max(120).optional(),
901
+ description: z.string().max(2000).optional(),
902
+ icon: z.string().max(64).nullable().optional(),
903
+ spaceId: Id.nullable().optional(),
904
+ variables: z.array(TemplateVariable).max(25).optional(),
905
+ /** take the body from this page (or space) again; omit to leave the body alone */
906
+ sourceId: Id.optional(),
907
+ }),
908
+ )
909
+ .output(Template),
910
+ /**
911
+ * Delete it. For a row that replaced a starter this is "reset": the shipped one comes back.
912
+ *
913
+ * Nothing made from a template is touched — a page is a page once it exists, and a template that
914
+ * took its pages with it would be a delete nobody could afford to press.
915
+ */
916
+ remove: baseContract
917
+ .route({ method: 'DELETE', path: '/templates/{templateId}', ...t('templates') })
918
+ .input(ws.extend({ templateId: Id }))
919
+ .output(Ok),
920
+ /**
921
+ * Make the thing: a page, or a whole space and its tree.
922
+ *
923
+ * Exactly one of `templateId` and `starterKey` — a starter is a constant with no id, and a
924
+ * union input would be a shape oRPC has to route. Both, or neither, is a bad request.
925
+ *
926
+ * `values` fills what the template declared. `{{date}}`, `{{time}}`, `{{author}}` and
927
+ * `{{space}}` are filled by the server from the request and are declared by nobody; a name the
928
+ * template never declared is left in the page as it was written, because deleting text somebody
929
+ * typed is worse than showing them a placeholder they can see and fix.
930
+ *
931
+ * Substitution happens **in text, never in JSON**. A value containing a quote, a brace or a
932
+ * newline is characters in a paragraph and cannot be anything else — see `services/templates.ts`.
933
+ */
934
+ instantiate: baseContract
935
+ .route({ method: 'POST', path: '/templates/instantiate', ...t('templates') })
936
+ .input(
937
+ ws.extend({
938
+ /** a saved template; null when `starterKey` names a shipped one */
939
+ templateId: Id.nullable().default(null),
940
+ /** a shipped starter; null when `templateId` names a saved one */
941
+ starterKey: TemplateStarterKey.nullable().default(null),
942
+ /** the space to write into, for a page template. Ignored for a space template. */
943
+ spaceId: Id.nullable().default(null),
944
+ /** where the new page hangs, for a page template */
945
+ parentId: Id.nullable().default(null),
946
+ afterId: Id.nullable().default(null),
947
+ /** what the new page is called; empty takes the template's own name */
948
+ title: z.string().max(300).default(''),
949
+ /** the new space's key and name, for a space template */
950
+ key: Space.shape.key.nullable().default(null),
951
+ name: z.string().max(120).default(''),
952
+ values: TemplateValues.default({}),
953
+ }),
954
+ )
955
+ .output(TemplateResult),
956
+ },
957
+
730
958
  publishing: {
731
959
  /** Make what is written now the version readers are served. Only meaningful for a `page`. */
732
960
  publish: baseContract