@kernhq/module-quire 0.13.1 → 0.15.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.
- package/dist/contract/models.d.ts +394 -0
- package/dist/contract/models.d.ts.map +1 -1
- package/dist/contract/models.js +284 -0
- package/dist/contract/models.js.map +1 -1
- package/dist/contract/permissions.d.ts +14 -0
- package/dist/contract/permissions.d.ts.map +1 -1
- package/dist/contract/permissions.js +114 -0
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/properties.d.ts +7 -7
- package/dist/contract/router.d.ts +1124 -96
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +343 -1
- package/dist/contract/router.js.map +1 -1
- package/dist/server/_impl.d.ts +983 -1168
- package/dist/server/_impl.d.ts.map +1 -1
- package/dist/server/_impl.js +308 -0
- package/dist/server/_impl.js.map +1 -1
- package/dist/server/export/html.d.ts +67 -0
- package/dist/server/export/html.d.ts.map +1 -0
- package/dist/server/export/html.js +206 -0
- package/dist/server/export/html.js.map +1 -0
- package/dist/server/export/markdown.d.ts +51 -0
- package/dist/server/export/markdown.d.ts.map +1 -0
- package/dist/server/export/markdown.js +351 -0
- package/dist/server/export/markdown.js.map +1 -0
- package/dist/server/export/pdf.d.ts +20 -0
- package/dist/server/export/pdf.d.ts.map +1 -0
- package/dist/server/export/pdf.js +91 -0
- package/dist/server/export/pdf.js.map +1 -0
- package/dist/server/export/zip.d.ts +31 -0
- package/dist/server/export/zip.d.ts.map +1 -0
- package/dist/server/export/zip.js +158 -0
- package/dist/server/export/zip.js.map +1 -0
- package/dist/server/import/csv.d.ts +77 -0
- package/dist/server/import/csv.d.ts.map +1 -0
- package/dist/server/import/csv.js +263 -0
- package/dist/server/import/csv.js.map +1 -0
- package/dist/server/import/html.d.ts +52 -0
- package/dist/server/import/html.d.ts.map +1 -0
- package/dist/server/import/html.js +472 -0
- package/dist/server/import/html.js.map +1 -0
- package/dist/server/import/markdown.d.ts +63 -0
- package/dist/server/import/markdown.d.ts.map +1 -0
- package/dist/server/import/markdown.js +692 -0
- package/dist/server/import/markdown.js.map +1 -0
- package/dist/server/import/plan.d.ts +70 -0
- package/dist/server/import/plan.d.ts.map +1 -0
- package/dist/server/import/plan.js +761 -0
- package/dist/server/import/plan.js.map +1 -0
- package/dist/server/import/ydoc.d.ts +35 -0
- package/dist/server/import/ydoc.d.ts.map +1 -0
- package/dist/server/import/ydoc.js +91 -0
- package/dist/server/import/ydoc.js.map +1 -0
- package/dist/server/import/zip.d.ts +63 -0
- package/dist/server/import/zip.d.ts.map +1 -0
- package/dist/server/import/zip.js +308 -0
- package/dist/server/import/zip.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/render.d.ts +158 -0
- package/dist/server/render.d.ts.map +1 -1
- package/dist/server/render.js +240 -0
- package/dist/server/render.js.map +1 -1
- package/dist/server/schema.d.ts +704 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +254 -1
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/databases.d.ts +10 -10
- package/dist/server/services/export.d.ts +176 -0
- package/dist/server/services/export.d.ts.map +1 -0
- package/dist/server/services/export.js +822 -0
- package/dist/server/services/export.js.map +1 -0
- package/dist/server/services/import.d.ts +109 -0
- package/dist/server/services/import.d.ts.map +1 -0
- package/dist/server/services/import.js +570 -0
- package/dist/server/services/import.js.map +1 -0
- package/dist/server/services/index.d.ts +32 -1
- package/dist/server/services/index.d.ts.map +1 -1
- package/dist/server/services/index.js +87 -4
- package/dist/server/services/index.js.map +1 -1
- package/dist/server/services/macros.d.ts +83 -0
- package/dist/server/services/macros.d.ts.map +1 -0
- package/dist/server/services/macros.js +488 -0
- package/dist/server/services/macros.js.map +1 -0
- package/dist/server/services/publications.d.ts +2 -1
- package/dist/server/services/publications.d.ts.map +1 -1
- package/dist/server/services/publications.js +43 -4
- package/dist/server/services/publications.js.map +1 -1
- package/dist/server/services/templates.d.ts +135 -0
- package/dist/server/services/templates.d.ts.map +1 -0
- package/dist/server/services/templates.js +897 -0
- package/dist/server/services/templates.js.map +1 -0
- package/dist/server/services/versions.d.ts +13 -1
- package/dist/server/services/versions.d.ts.map +1 -1
- package/dist/server/services/versions.js +3 -1
- package/dist/server/services/versions.js.map +1 -1
- package/migrations/0010_transfers.sql +154 -0
- package/migrations/0011_templates.sql +157 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +5 -5
- package/src/client/components/ExportDialog.svelte +685 -0
- package/src/client/components/ImportDialog.svelte +702 -0
- package/src/client/components/ImportReport.svelte +310 -0
- package/src/client/components/NewSpaceDialog.svelte +77 -8
- package/src/client/components/PageEditor.svelte +80 -0
- package/src/client/components/PagePicker.svelte +264 -0
- package/src/client/components/SaveAsTemplateDialog.svelte +502 -0
- package/src/client/components/SidebarSpaces.svelte +112 -1
- package/src/client/components/TemplatePicker.svelte +437 -0
- package/src/client/i18n.ts +941 -0
- package/src/client/index.ts +51 -0
- package/src/client/mock.ts +592 -0
- package/src/client/module.ts +36 -0
- package/src/client/pages/PageView.svelte +86 -0
- package/src/client/pages/SpacePage.svelte +20 -4
- package/src/client/pages/TransfersPage.svelte +570 -0
- package/src/client/permissions.ts +11 -0
- package/src/client/query.ts +40 -0
- package/src/client/transfers.ts +142 -0
- package/src/contract/models.ts +343 -0
- package/src/contract/permissions.ts +117 -0
- package/src/contract/router.ts +375 -0
package/src/contract/router.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { z } from 'zod'
|
|
|
3
3
|
import {
|
|
4
4
|
CommentAnchor,
|
|
5
5
|
CommentThread,
|
|
6
|
+
ExportFormat,
|
|
7
|
+
ExportJob,
|
|
8
|
+
ExportScope,
|
|
6
9
|
Favorite,
|
|
10
|
+
ImportJob,
|
|
11
|
+
ImportSource,
|
|
7
12
|
Label,
|
|
8
13
|
LabelColour,
|
|
9
14
|
Ok,
|
|
@@ -17,6 +22,10 @@ import {
|
|
|
17
22
|
RichDoc,
|
|
18
23
|
Space,
|
|
19
24
|
SpaceVisibility,
|
|
25
|
+
Template,
|
|
26
|
+
TemplateKind,
|
|
27
|
+
TemplateStarterKey,
|
|
28
|
+
TemplateVariable,
|
|
20
29
|
} from './models.js'
|
|
21
30
|
import {
|
|
22
31
|
Database,
|
|
@@ -184,6 +193,34 @@ export type PublicSearchHit = z.infer<typeof PublicSearchHit>
|
|
|
184
193
|
|
|
185
194
|
export const PublicSitemapEntry = z.object({ path: z.string(), lastModified: Timestamp })
|
|
186
195
|
|
|
196
|
+
/**
|
|
197
|
+
* An export job, plus the one thing that cannot be stored on it: a link to the artefact.
|
|
198
|
+
*
|
|
199
|
+
* `downloadUrl` is minted per request and is null in every state but `done`. It is composed here
|
|
200
|
+
* rather than written into `export_jobs` because a signed storage URL is the object's key, so a
|
|
201
|
+
* stored one is both an address that leaks the workspace and file uuids and an address that stops
|
|
202
|
+
* working an hour later — and, worse, a fence applied once at the moment the job finished rather
|
|
203
|
+
* than every time somebody asks. A subtree export flattens pages of different readerships into one
|
|
204
|
+
* file, so the moment of the fetch is the moment that has to be checked.
|
|
205
|
+
*/
|
|
206
|
+
export const ExportJobDetail = ExportJob.extend({ downloadUrl: z.string().nullable() })
|
|
207
|
+
export type ExportJobDetail = z.infer<typeof ExportJobDetail>
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* An import job **without its report**, which is the only shape a list can afford to carry.
|
|
211
|
+
*
|
|
212
|
+
* The report is one row per file, and a Notion export is thousands of files — so twenty jobs in a
|
|
213
|
+
* list is a response of megabytes to draw a table of dates and states. `get` carries the whole thing
|
|
214
|
+
* because that screen is somebody looking at one import and asking what happened to their files,
|
|
215
|
+
* which is the question the report exists to answer.
|
|
216
|
+
*
|
|
217
|
+
* Omitted rather than truncated on purpose. A report cut off at fifty rows is one that has quietly
|
|
218
|
+
* stopped being the complete account of the archive, and a person reading "23 skipped" beside a list
|
|
219
|
+
* of twelve would have no way to know which. Absent is a state a client can render; incomplete is not.
|
|
220
|
+
*/
|
|
221
|
+
export const ImportJobSummary = ImportJob.omit({ report: true })
|
|
222
|
+
export type ImportJobSummary = z.infer<typeof ImportJobSummary>
|
|
223
|
+
|
|
187
224
|
/**
|
|
188
225
|
* One picture from a published page, **as bytes rather than as an address**.
|
|
189
226
|
*
|
|
@@ -254,6 +291,77 @@ export const PublicBasePath = z
|
|
|
254
291
|
*/
|
|
255
292
|
export const PUBLIC_ASSET_SEGMENT = '__media'
|
|
256
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
|
+
|
|
257
365
|
export const quireContract = {
|
|
258
366
|
spaces: {
|
|
259
367
|
list: baseContract
|
|
@@ -694,6 +802,159 @@ export const quireContract = {
|
|
|
694
802
|
.output(Ok),
|
|
695
803
|
},
|
|
696
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
|
+
|
|
697
958
|
publishing: {
|
|
698
959
|
/** Make what is written now the version readers are served. Only meaningful for a `page`. */
|
|
699
960
|
publish: baseContract
|
|
@@ -781,6 +1042,120 @@ export const quireContract = {
|
|
|
781
1042
|
.output(z.object({ pageId: Id, excluded: z.boolean() })),
|
|
782
1043
|
},
|
|
783
1044
|
|
|
1045
|
+
/**
|
|
1046
|
+
* Taking work out: a page, a page and everything under it, or a whole space, as a file.
|
|
1047
|
+
*
|
|
1048
|
+
* Three procedures and no fourth, which is the shape worth explaining. There is no `download`:
|
|
1049
|
+
* `get` carries a `downloadUrl` it mints as it answers, so the permission is checked on the
|
|
1050
|
+
* request that fetches the file rather than baked into a link that outlives the check. And there
|
|
1051
|
+
* is no `cancel`: a job either finishes or fails, both terminal, and a cancel that races a worker
|
|
1052
|
+
* writing an artefact is a way to end up with an object nothing points at.
|
|
1053
|
+
*
|
|
1054
|
+
* **What the artefact contains is decided by the job, not by the caller.** `start` names a scope
|
|
1055
|
+
* and a format; every page under that scope is then checked against the requester's own
|
|
1056
|
+
* `quire.page.view`, and a page they may not read is left out and counted in `skipped`. So a
|
|
1057
|
+
* subtree export by somebody with a page-scoped DENY is a smaller file, not a refusal — and
|
|
1058
|
+
* `counts.skipped` is how they find out, which is the difference between an export that is quietly
|
|
1059
|
+
* missing pages and one that says how many.
|
|
1060
|
+
*
|
|
1061
|
+
* `format: 'docx'` is declared in `ExportFormat` and refused by `start` today: see the note in
|
|
1062
|
+
* `services/export.ts` for what makes a *correct* Word file more than a matter of effort, and why
|
|
1063
|
+
* a refusal is better than a document that may not open.
|
|
1064
|
+
*/
|
|
1065
|
+
exports: {
|
|
1066
|
+
/** Queue one. The answer is a row to watch, never the file — see the note above. */
|
|
1067
|
+
start: baseContract
|
|
1068
|
+
.route({ method: 'POST', path: '/exports', ...t('exports') })
|
|
1069
|
+
.input(
|
|
1070
|
+
ws.extend({
|
|
1071
|
+
scope: ExportScope,
|
|
1072
|
+
/** the page for `page` and `subtree`, the space for `space`; `scope` says which */
|
|
1073
|
+
targetId: Id,
|
|
1074
|
+
format: ExportFormat,
|
|
1075
|
+
}),
|
|
1076
|
+
)
|
|
1077
|
+
.output(ExportJobDetail),
|
|
1078
|
+
/** Where it has got to, and — once it is `done` — a link that is good for a few minutes. */
|
|
1079
|
+
get: baseContract
|
|
1080
|
+
.route({ method: 'GET', path: '/exports/{jobId}', ...t('exports') })
|
|
1081
|
+
.input(ws.extend({ jobId: Id }))
|
|
1082
|
+
.output(ExportJobDetail),
|
|
1083
|
+
/**
|
|
1084
|
+
* This person's own exports, newest first.
|
|
1085
|
+
*
|
|
1086
|
+
* Deliberately not the workspace's. Row-level security fences the tenant, which is not a privacy
|
|
1087
|
+
* boundary, so the `requested_by` filter in the query is the only thing that keeps one person's
|
|
1088
|
+
* export of the salary handbook out of everybody else's list — the same rule `favorites.list`
|
|
1089
|
+
* and `recents.list` follow.
|
|
1090
|
+
*/
|
|
1091
|
+
list: baseContract
|
|
1092
|
+
.route({ method: 'GET', path: '/exports', ...t('exports') })
|
|
1093
|
+
.input(ws.extend({ limit: z.number().int().min(1).max(50).default(20) }))
|
|
1094
|
+
.output(z.array(ExportJob)),
|
|
1095
|
+
},
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Getting work in: a Notion export, a Confluence export or a folder of Markdown, into one space.
|
|
1099
|
+
*
|
|
1100
|
+
* **The failure list is the feature.** A real export has files that will not map — an attachment,
|
|
1101
|
+
* a `.csv` with no header, a page whose link points outside what was exported — and an import that
|
|
1102
|
+
* silently drops forty pages is worse than one that refuses. So every file in the archive gets a
|
|
1103
|
+
* row in `report` saying whether it became a page, was deliberately left out, or could not be read,
|
|
1104
|
+
* and `counts.total` is exactly the number of rows: nothing in the archive goes unaccounted for.
|
|
1105
|
+
*
|
|
1106
|
+
* Three things follow from the archive being read whole before anything is written, and each is
|
|
1107
|
+
* worth knowing before reading the handlers:
|
|
1108
|
+
*
|
|
1109
|
+
* - **an import is all or nothing.** A zip that fails half way leaves the space exactly as it
|
|
1110
|
+
* was, because the plan is built in memory and written in one transaction;
|
|
1111
|
+
* - **links between imported pages are rewritten to Quire page ids**, which needs every id to
|
|
1112
|
+
* exist before any body is resolved — a link to a page further down the archive is the normal
|
|
1113
|
+
* case, not the exception. A link that resolves to nothing becomes plain text rather than a
|
|
1114
|
+
* dead link, and the report names the target it could not find;
|
|
1115
|
+
* - **a `.csv` becomes a database with typed columns**, guessed from the values and reported.
|
|
1116
|
+
*
|
|
1117
|
+
* `quire.page.import` is `dangerous`, as the tracker marks its own imports: this is the one thing
|
|
1118
|
+
* in the module that writes hundreds of pages into a space in one act, and it is not undone by
|
|
1119
|
+
* pressing something.
|
|
1120
|
+
*/
|
|
1121
|
+
imports: {
|
|
1122
|
+
/**
|
|
1123
|
+
* Queue one. The answer is a row to watch; nothing has been written to the space yet.
|
|
1124
|
+
*
|
|
1125
|
+
* `fileId` is an upload — a core file the browser has already put in place — and not the archive
|
|
1126
|
+
* itself. A zip is up to a few hundred megabytes, which is a file to be uploaded and then named,
|
|
1127
|
+
* never a request body.
|
|
1128
|
+
*/
|
|
1129
|
+
start: baseContract
|
|
1130
|
+
.route({ method: 'POST', path: '/imports', ...t('imports') })
|
|
1131
|
+
.input(
|
|
1132
|
+
ws.extend({
|
|
1133
|
+
/** the space being written into; an import always targets exactly one */
|
|
1134
|
+
spaceId: Id,
|
|
1135
|
+
source: ImportSource,
|
|
1136
|
+
/** the uploaded archive, and not something this job produces — see `ImportJob` */
|
|
1137
|
+
fileId: Id,
|
|
1138
|
+
}),
|
|
1139
|
+
)
|
|
1140
|
+
.output(ImportJob),
|
|
1141
|
+
/** Where it has got to, and — once it has finished — what happened to every file in it. */
|
|
1142
|
+
get: baseContract
|
|
1143
|
+
.route({ method: 'GET', path: '/imports/{jobId}', ...t('imports') })
|
|
1144
|
+
.input(ws.extend({ jobId: Id }))
|
|
1145
|
+
.output(ImportJob),
|
|
1146
|
+
/**
|
|
1147
|
+
* This person's own imports, newest first, without their reports — see `ImportJobSummary`.
|
|
1148
|
+
*
|
|
1149
|
+
* Deliberately not the workspace's, for the same reason `exports.list` is not: row-level security
|
|
1150
|
+
* fences the tenant, which is not a privacy boundary, so the `requested_by` filter in the query
|
|
1151
|
+
* is the only thing that keeps one person's import out of everybody else's list.
|
|
1152
|
+
*/
|
|
1153
|
+
list: baseContract
|
|
1154
|
+
.route({ method: 'GET', path: '/imports', ...t('imports') })
|
|
1155
|
+
.input(ws.extend({ limit: z.number().int().min(1).max(50).default(20) }))
|
|
1156
|
+
.output(z.array(ImportJobSummary)),
|
|
1157
|
+
},
|
|
1158
|
+
|
|
784
1159
|
/**
|
|
785
1160
|
* The signed-out surface. **This is the only part of Kern with no principal behind it.**
|
|
786
1161
|
*
|