@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_impl.d.ts","sourceRoot":"","sources":["../../src/server/_impl.ts"],"names":[],"mappings":"AACA,OAAO,EACL,YAAY,EACZ,kBAAkB,EAElB,KAAK,MAAM,EAEX,cAAc,EACd,KAAK,cAAc,EAIpB,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"_impl.d.ts","sourceRoot":"","sources":["../../src/server/_impl.ts"],"names":[],"mappings":"AACA,OAAO,EACL,YAAY,EACZ,kBAAkB,EAElB,KAAK,MAAM,EAEX,cAAc,EACd,KAAK,cAAc,EAIpB,MAAM,gBAAgB,CAAA;AAYvB;;;;;;;GAOG;AACH,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,CAAA;AAqF3D,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2oDxC"}
|
package/dist/server/_impl.js
CHANGED
|
@@ -7,6 +7,7 @@ import { quireServices, resolveWorkspaceSegment } from './services/index.js';
|
|
|
7
7
|
import { createNotify } from './services/notify.js';
|
|
8
8
|
import { documentNameOf, toPage } from './services/pages.js';
|
|
9
9
|
import { toPublication } from './services/publications.js';
|
|
10
|
+
import { toTemplate } from './services/templates.js';
|
|
10
11
|
import { toVersion } from './services/versions.js';
|
|
11
12
|
/**
|
|
12
13
|
* The router, kept apart from `index.ts` so `module.test.ts` can walk it without booting a kernel.
|
|
@@ -154,6 +155,19 @@ export function implement_(kernel) {
|
|
|
154
155
|
};
|
|
155
156
|
/** Both, every time: the event for anything that reacts later, the change for a screen open now. */
|
|
156
157
|
const announce = (workspaceId, entity, id, op, scope) => kernel.realtime.change(workspaceId, { module: MODULE_ID, entity, id, op, scope });
|
|
158
|
+
/**
|
|
159
|
+
* The same, for the one entity a workspace-wide broadcast should not carry.
|
|
160
|
+
*
|
|
161
|
+
* A transfer is fenced to the person who asked for it everywhere else — `list` returns only their
|
|
162
|
+
* rows, `get` answers NOT_FOUND rather than FORBIDDEN for anyone else's id — and announcing it on
|
|
163
|
+
* the workspace channel handed every member the id and the moment it was created, which is the
|
|
164
|
+
* fact the NOT_FOUND exists to withhold. `services/export.ts` carries the long version.
|
|
165
|
+
*/
|
|
166
|
+
const announceToRequester = (workspaceId, userId, entity, id, op) => kernel.realtime.toUser(userId, {
|
|
167
|
+
t: 'change',
|
|
168
|
+
workspaceId: workspaceId,
|
|
169
|
+
change: { module: MODULE_ID, entity, id, op },
|
|
170
|
+
});
|
|
157
171
|
return os.router({
|
|
158
172
|
spaces: {
|
|
159
173
|
list: scoped.spaces.list
|
|
@@ -757,6 +771,300 @@ export function implement_(kernel) {
|
|
|
757
771
|
return { ok: true };
|
|
758
772
|
})),
|
|
759
773
|
},
|
|
774
|
+
/**
|
|
775
|
+
* Taking work out.
|
|
776
|
+
*
|
|
777
|
+
* The handler is deliberately thin, because nothing about an export can be decided here: which
|
|
778
|
+
* pages go in is a per-page permission question asked as the requester against a tree that may
|
|
779
|
+
* be five hundred deep, and rendering one is a round trip to storage and possibly to Chromium.
|
|
780
|
+
* All three of those outlive an HTTP request, so this records a row, hands it to a worker, and
|
|
781
|
+
* answers with something to watch.
|
|
782
|
+
*
|
|
783
|
+
* The one thing it *does* decide is whether this person may ask at all, and that is asked twice
|
|
784
|
+
* over: `requires` puts the workspace-level gate on the procedure, and the handler resolves the
|
|
785
|
+
* target — a page's own ancestor chain, or the space — and asks again at the narrow scope. The
|
|
786
|
+
* second check is the one a page-scoped DENY can reach.
|
|
787
|
+
*/
|
|
788
|
+
exports: {
|
|
789
|
+
start: scoped.exports.start.use(requires('quire.page.export')).handler(async ({ input, context }) => {
|
|
790
|
+
const row = await run(context, input.workspaceId, async (tx) => {
|
|
791
|
+
if (input.scope === 'space') {
|
|
792
|
+
await svc.access.spaceRow(tx, input.workspaceId, input.targetId);
|
|
793
|
+
await svc.access.requireSpace(context.principal, 'quire.page.export', input.workspaceId, input.targetId);
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
await requirePage(tx, context, input.workspaceId, input.targetId, 'quire.page.export');
|
|
797
|
+
}
|
|
798
|
+
// Expired artefacts go now, and jobs that lost their worker are failed now, in the
|
|
799
|
+
// workspace whose transaction is already open — see the notes on `sweep` and `reap` for
|
|
800
|
+
// why neither is a cron job. Neither may take a request down, hence the catch.
|
|
801
|
+
await svc.exports.sweep(tx, input.workspaceId).catch(() => 0);
|
|
802
|
+
await svc.exports.reap(tx, input.workspaceId).catch(() => 0);
|
|
803
|
+
return svc.exports.start(tx, context.principal, input.workspaceId, input);
|
|
804
|
+
});
|
|
805
|
+
/*
|
|
806
|
+
* Sent after the row is committed, so the worker cannot reach for a row that is not there
|
|
807
|
+
* yet. If the queue refuses the job the row would otherwise sit `queued` for ever, looking
|
|
808
|
+
* like work in progress — so it is failed here, with the reason, rather than left hanging.
|
|
809
|
+
*/
|
|
810
|
+
try {
|
|
811
|
+
await kernel.jobs.send('quire.export', { workspaceId: input.workspaceId, jobId: row.id });
|
|
812
|
+
}
|
|
813
|
+
catch (err) {
|
|
814
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
815
|
+
kernel.log.error({ err: message, jobId: row.id }, 'quire: an export could not be queued');
|
|
816
|
+
await run(context, input.workspaceId, (tx) => svc.exports.fail(tx, input.workspaceId, row.id, message));
|
|
817
|
+
throw new KernError('UNAVAILABLE', 'The export could not be queued. Try again in a moment.');
|
|
818
|
+
}
|
|
819
|
+
await announceToRequester(input.workspaceId, row.requestedBy, 'export', row.id, 'created');
|
|
820
|
+
return { ...svc.exports.toExportJob(row), downloadUrl: null };
|
|
821
|
+
}),
|
|
822
|
+
get: scoped.exports.get.use(requires('quire.page.export')).handler(({ input, context }) => run(context, input.workspaceId, async (tx) => {
|
|
823
|
+
const row = await svc.exports.get(tx, input.workspaceId, input.jobId, context.principal);
|
|
824
|
+
return {
|
|
825
|
+
...svc.exports.toExportJob(row),
|
|
826
|
+
downloadUrl: await svc.exports.downloadUrl(tx, input.workspaceId, row),
|
|
827
|
+
};
|
|
828
|
+
})),
|
|
829
|
+
list: scoped.exports.list.use(requires('quire.page.export')).handler(({ input, context }) => run(context, input.workspaceId, async (tx) => {
|
|
830
|
+
await svc.exports.sweep(tx, input.workspaceId).catch(() => 0);
|
|
831
|
+
// This list is polled every two seconds while anything on it is running, so it is the
|
|
832
|
+
// thing that ends a job whose worker went away rather than drawing its spinner for ever.
|
|
833
|
+
await svc.exports.reap(tx, input.workspaceId).catch(() => 0);
|
|
834
|
+
const rows = await svc.exports.list(tx, input.workspaceId, context.principal, input.limit);
|
|
835
|
+
return rows.map(svc.exports.toExportJob);
|
|
836
|
+
})),
|
|
837
|
+
},
|
|
838
|
+
imports: {
|
|
839
|
+
/**
|
|
840
|
+
* Queue one.
|
|
841
|
+
*
|
|
842
|
+
* The permission is checked here **and** again inside the job, and the two are not the same
|
|
843
|
+
* check. This one refuses a request before a row exists; the one in the job refuses to write
|
|
844
|
+
* after the queue has held the work for a minute, which is where a permission taken away in
|
|
845
|
+
* the meantime would otherwise be missed. `services/import.ts` does both, so the order they
|
|
846
|
+
* happen in cannot drift between the router and the worker.
|
|
847
|
+
*/
|
|
848
|
+
start: scoped.imports.start.use(requires('quire.page.import')).handler(async ({ input, context }) => {
|
|
849
|
+
const row = await run(context, input.workspaceId, async (tx) => {
|
|
850
|
+
// Jobs that lost their worker are failed now, in the workspace whose transaction is
|
|
851
|
+
// already open — see `reap` for why this is not a cron job, and why it marks rather than
|
|
852
|
+
// deletes. It must not take the request down, hence the catch.
|
|
853
|
+
await svc.imports.reap(tx, input.workspaceId).catch(() => 0);
|
|
854
|
+
return svc.imports.start(tx, context.principal, input.workspaceId, input);
|
|
855
|
+
});
|
|
856
|
+
/*
|
|
857
|
+
* Sent after the row is committed, so the worker cannot reach for a row that is not there
|
|
858
|
+
* yet. If the queue refuses the job the row would otherwise sit `queued` for ever, looking
|
|
859
|
+
* like an import in progress to somebody waiting to see whether their pages arrived — so it
|
|
860
|
+
* is failed here, with the reason, rather than left hanging.
|
|
861
|
+
*/
|
|
862
|
+
try {
|
|
863
|
+
await kernel.jobs.send('quire.import', { workspaceId: input.workspaceId, jobId: row.id });
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
867
|
+
kernel.log.error({ err: message, jobId: row.id }, 'quire: an import could not be queued');
|
|
868
|
+
await run(context, input.workspaceId, (tx) => svc.imports.fail(tx, input.workspaceId, row.id, message));
|
|
869
|
+
throw new KernError('UNAVAILABLE', 'The import could not be queued. Try again in a moment.');
|
|
870
|
+
}
|
|
871
|
+
await announceToRequester(input.workspaceId, row.requestedBy, 'import', row.id, 'created');
|
|
872
|
+
return svc.imports.toImportJob(row);
|
|
873
|
+
}),
|
|
874
|
+
get: scoped.imports.get
|
|
875
|
+
.use(requires('quire.page.import'))
|
|
876
|
+
.handler(({ input, context }) => run(context, input.workspaceId, async (tx) => svc.imports.toImportJob(await svc.imports.get(tx, input.workspaceId, input.jobId, context.principal)))),
|
|
877
|
+
/** Without the reports — a list of thousands-of-rows reports is megabytes to draw a table. */
|
|
878
|
+
list: scoped.imports.list.use(requires('quire.page.import')).handler(({ input, context }) => run(context, input.workspaceId, async (tx) => {
|
|
879
|
+
// Polled every two seconds while anything on it is running, so it is the thing that ends
|
|
880
|
+
// a job whose worker went away rather than drawing its spinner for ever.
|
|
881
|
+
await svc.imports.reap(tx, input.workspaceId).catch(() => 0);
|
|
882
|
+
const rows = await svc.imports.list(tx, input.workspaceId, context.principal, input.limit);
|
|
883
|
+
return rows.map((row) => {
|
|
884
|
+
const { report, ...summary } = svc.imports.toImportJob(row);
|
|
885
|
+
return summary;
|
|
886
|
+
});
|
|
887
|
+
})),
|
|
888
|
+
},
|
|
889
|
+
/**
|
|
890
|
+
* What somebody writes with.
|
|
891
|
+
*
|
|
892
|
+
* Two permissions and a rule for telling them apart: **using** a template is `quire.page.create`,
|
|
893
|
+
* because what it does is make a page; **changing** one is `quire.space.manage`, because it
|
|
894
|
+
* changes what everybody in the space is offered the next time they make one. That is exactly
|
|
895
|
+
* how `labels.*` above is split, and for the same reason.
|
|
896
|
+
*
|
|
897
|
+
* A template id carries no scope of its own, so every procedure here that takes one reads the
|
|
898
|
+
* row first and asks about the space it belongs to. A **workspace-wide** template
|
|
899
|
+
* (`space_id is null`) has no narrower scope than the workspace, and the workspace-level
|
|
900
|
+
* `requires()` is the whole answer for it — said out loud here rather than left as a space check
|
|
901
|
+
* that silently falls back to the space of whoever happened to ask.
|
|
902
|
+
*/
|
|
903
|
+
templates: {
|
|
904
|
+
list: scoped.templates.list.use(requires('quire.space.view')).handler(({ input, context }) => run(context, input.workspaceId, async (tx) => {
|
|
905
|
+
/*
|
|
906
|
+
* A space named is a space checked. Without one the question is "what is offered
|
|
907
|
+
* everywhere", which the "New space" picker asks before there is a space to ask about —
|
|
908
|
+
* and `requires()` is the only scope that exists for it.
|
|
909
|
+
*/
|
|
910
|
+
if (input.spaceId) {
|
|
911
|
+
await svc.access.spaceRow(tx, input.workspaceId, input.spaceId);
|
|
912
|
+
await svc.access.requireSpace(context.principal, 'quire.space.view', input.workspaceId, input.spaceId);
|
|
913
|
+
}
|
|
914
|
+
return svc.templates.list(tx, input.workspaceId, input.kind, input.spaceId, context.principal.locale);
|
|
915
|
+
})),
|
|
916
|
+
get: scoped.templates.get.use(requires('quire.space.view')).handler(({ input, context }) => run(context, input.workspaceId, async (tx) => {
|
|
917
|
+
const row = await svc.templates.row(tx, input.workspaceId, input.templateId);
|
|
918
|
+
if (row.spaceId) {
|
|
919
|
+
await svc.access.spaceRow(tx, input.workspaceId, row.spaceId);
|
|
920
|
+
await svc.access.requireSpace(context.principal, 'quire.space.view', input.workspaceId, row.spaceId);
|
|
921
|
+
}
|
|
922
|
+
return toTemplate(row);
|
|
923
|
+
})),
|
|
924
|
+
/**
|
|
925
|
+
* Two questions, not one, and the first is the one that protects anything.
|
|
926
|
+
*
|
|
927
|
+
* This copies a page's prose into something everybody who may create a page can then read, so
|
|
928
|
+
* **may you read what you are copying** is asked against the page's own ancestor chain — a
|
|
929
|
+
* contractor allowed one page of a handbook must not be able to lift a neighbouring page into
|
|
930
|
+
* a template and read it there. Only then is "may you configure where it is offered" asked.
|
|
931
|
+
*/
|
|
932
|
+
createFromPage: scoped.templates.createFromPage
|
|
933
|
+
.use(requires('quire.space.manage'))
|
|
934
|
+
.handler(async ({ input, context }) => {
|
|
935
|
+
const template = await run(context, input.workspaceId, async (tx) => {
|
|
936
|
+
if (input.kind === 'space') {
|
|
937
|
+
await svc.access.spaceRow(tx, input.workspaceId, input.sourceId);
|
|
938
|
+
await svc.access.requireSpace(context.principal, 'quire.space.manage', input.workspaceId, input.sourceId);
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
await requirePage(tx, context, input.workspaceId, input.sourceId, 'quire.page.view');
|
|
942
|
+
if (input.spaceId)
|
|
943
|
+
await svc.access.requireSpace(context.principal, 'quire.space.manage', input.workspaceId, input.spaceId);
|
|
944
|
+
}
|
|
945
|
+
return svc.templates.createFromPage(tx, context.principal, input.workspaceId, input);
|
|
946
|
+
});
|
|
947
|
+
await announce(input.workspaceId, 'template', template.id, 'created');
|
|
948
|
+
return template;
|
|
949
|
+
}),
|
|
950
|
+
update: scoped.templates.update
|
|
951
|
+
.use(requires('quire.space.manage'))
|
|
952
|
+
.handler(async ({ input, context }) => {
|
|
953
|
+
const { workspaceId, templateId, ...patch } = input;
|
|
954
|
+
const template = await run(context, workspaceId, async (tx) => {
|
|
955
|
+
const existing = await svc.templates.row(tx, workspaceId, templateId);
|
|
956
|
+
if (existing.spaceId)
|
|
957
|
+
await svc.access.requireSpace(context.principal, 'quire.space.manage', workspaceId, existing.spaceId);
|
|
958
|
+
// Moving it into a space needs the same permission on the space it is moving into, or
|
|
959
|
+
// "offered everywhere" would be a way to put a template in a space you may not configure.
|
|
960
|
+
if (patch.spaceId)
|
|
961
|
+
await svc.access.requireSpace(context.principal, 'quire.space.manage', workspaceId, patch.spaceId);
|
|
962
|
+
/*
|
|
963
|
+
* Replacing the body re-reads a page or a space, so the read is checked again exactly as
|
|
964
|
+
* it is in `createFromPage`. The same act needs the same permission whichever door it
|
|
965
|
+
* comes in by, and this is the door that is easy to forget: `update` looks like a rename.
|
|
966
|
+
*/
|
|
967
|
+
if (patch.sourceId !== undefined) {
|
|
968
|
+
if (existing.kind === 'space') {
|
|
969
|
+
await svc.access.spaceRow(tx, workspaceId, patch.sourceId);
|
|
970
|
+
await svc.access.requireSpace(context.principal, 'quire.space.manage', workspaceId, patch.sourceId);
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
await requirePage(tx, context, workspaceId, patch.sourceId, 'quire.page.view');
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return svc.templates.update(tx, context.principal, workspaceId, templateId, patch);
|
|
977
|
+
});
|
|
978
|
+
await announce(workspaceId, 'template', template.id, 'updated');
|
|
979
|
+
return template;
|
|
980
|
+
}),
|
|
981
|
+
remove: scoped.templates.remove
|
|
982
|
+
.use(requires('quire.space.manage'))
|
|
983
|
+
.handler(async ({ input, context }) => {
|
|
984
|
+
await run(context, input.workspaceId, async (tx) => {
|
|
985
|
+
const existing = await svc.templates.row(tx, input.workspaceId, input.templateId);
|
|
986
|
+
if (existing.spaceId)
|
|
987
|
+
await svc.access.requireSpace(context.principal, 'quire.space.manage', input.workspaceId, existing.spaceId);
|
|
988
|
+
await svc.templates.remove(tx, input.workspaceId, input.templateId);
|
|
989
|
+
});
|
|
990
|
+
await announce(input.workspaceId, 'template', input.templateId, 'deleted');
|
|
991
|
+
return { ok: true };
|
|
992
|
+
}),
|
|
993
|
+
/**
|
|
994
|
+
* Make the thing.
|
|
995
|
+
*
|
|
996
|
+
* The kind decides which permission is asked, and the template decides the kind — so the
|
|
997
|
+
* template is resolved before anything is checked. A page template asks `quire.page.create` on
|
|
998
|
+
* the space it writes into; a space template *creates* a space, so it asks
|
|
999
|
+
* `quire.space.manage` at workspace scope, which is the same question `spaces.create` asks and
|
|
1000
|
+
* the only scope that exists before a space does.
|
|
1001
|
+
*/
|
|
1002
|
+
instantiate: scoped.templates.instantiate
|
|
1003
|
+
.use(requires('quire.page.create'))
|
|
1004
|
+
.handler(async ({ input, context }) => {
|
|
1005
|
+
/*
|
|
1006
|
+
* Which branch ran, recorded rather than inferred from the answer.
|
|
1007
|
+
*
|
|
1008
|
+
* `TemplateResult` is one shape for both kinds on purpose, so nothing in it says "a space
|
|
1009
|
+
* was made" — and guessing from `pageCount` would be wrong for the two cases that matter:
|
|
1010
|
+
* a space template whose tree is one page, and one whose tree is empty.
|
|
1011
|
+
*/
|
|
1012
|
+
let madeSpace = false;
|
|
1013
|
+
const result = await run(context, input.workspaceId, async (tx) => {
|
|
1014
|
+
const resolved = await svc.templates.resolve(tx, input.workspaceId, context.principal.locale, input.templateId, input.starterKey);
|
|
1015
|
+
if (resolved.kind === 'space') {
|
|
1016
|
+
madeSpace = true;
|
|
1017
|
+
if (!input.key || !input.name)
|
|
1018
|
+
throw KernError.badRequest('A space template needs a name and an address for the space');
|
|
1019
|
+
await kernel.authz.require(context.principal, 'quire.space.manage', {
|
|
1020
|
+
kind: 'workspace',
|
|
1021
|
+
id: input.workspaceId,
|
|
1022
|
+
workspaceId: input.workspaceId,
|
|
1023
|
+
});
|
|
1024
|
+
return svc.templates.instantiateSpace(tx, context.principal, input.workspaceId, resolved, {
|
|
1025
|
+
key: input.key,
|
|
1026
|
+
name: input.name,
|
|
1027
|
+
values: input.values,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
if (!input.spaceId)
|
|
1031
|
+
throw KernError.badRequest('A page template needs a space to be made in');
|
|
1032
|
+
await svc.access.spaceRow(tx, input.workspaceId, input.spaceId);
|
|
1033
|
+
await svc.access.requireSpace(context.principal, 'quire.page.create', input.workspaceId, input.spaceId);
|
|
1034
|
+
return svc.templates.instantiatePage(tx, context.principal, input.workspaceId, resolved, {
|
|
1035
|
+
spaceId: input.spaceId,
|
|
1036
|
+
parentId: input.parentId,
|
|
1037
|
+
afterId: input.afterId,
|
|
1038
|
+
title: input.title,
|
|
1039
|
+
values: input.values,
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
/*
|
|
1043
|
+
* A space template makes a space *and* a tree, so both are announced — the space list and
|
|
1044
|
+
* one space's tree are different screens, and announcing only the pages would leave the
|
|
1045
|
+
* space list empty until somebody reloaded.
|
|
1046
|
+
*/
|
|
1047
|
+
if (madeSpace) {
|
|
1048
|
+
await announce(input.workspaceId, 'space', result.spaceId, 'created');
|
|
1049
|
+
await kernel.emit(quireEvents.spaceCreated, { spaceId: result.spaceId, workspaceId: input.workspaceId }, { workspaceId: input.workspaceId, actorId: context.principal.userId });
|
|
1050
|
+
}
|
|
1051
|
+
/*
|
|
1052
|
+
* One event, for the page somebody is about to be taken to.
|
|
1053
|
+
*
|
|
1054
|
+
* A space template that made eleven pages emits one `pageCreated` and not eleven: an event
|
|
1055
|
+
* is what something *reacts* to, and eleven notifications for one act is the shape that
|
|
1056
|
+
* teaches people to mute a feed. `pageCount` in the answer is how a screen says how many.
|
|
1057
|
+
* A tree with no pages emits nothing rather than an event carrying an empty page id.
|
|
1058
|
+
*/
|
|
1059
|
+
if (result.pageId) {
|
|
1060
|
+
await announce(input.workspaceId, 'page', result.pageId, 'created', {
|
|
1061
|
+
spaceId: result.spaceId,
|
|
1062
|
+
});
|
|
1063
|
+
await kernel.emit(quireEvents.pageCreated, { pageId: result.pageId, spaceId: result.spaceId, workspaceId: input.workspaceId }, { workspaceId: input.workspaceId, actorId: context.principal.userId });
|
|
1064
|
+
}
|
|
1065
|
+
return result;
|
|
1066
|
+
}),
|
|
1067
|
+
},
|
|
760
1068
|
publishing: {
|
|
761
1069
|
publish: scoped.publishing.publish
|
|
762
1070
|
.use(requires('quire.page.publish'))
|