@kernhq/module-quire 0.13.1 → 0.14.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 +185 -0
- package/dist/contract/models.d.ts.map +1 -1
- package/dist/contract/models.js +133 -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 +82 -0
- package/dist/contract/permissions.js.map +1 -1
- package/dist/contract/properties.d.ts +4 -4
- package/dist/contract/router.d.ts +535 -8
- package/dist/contract/router.d.ts.map +1 -1
- package/dist/contract/router.js +133 -1
- package/dist/contract/router.js.map +1 -1
- package/dist/server/_impl.d.ts +519 -8
- package/dist/server/_impl.d.ts.map +1 -1
- package/dist/server/_impl.js +128 -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 +312 -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/schema.d.ts +445 -1
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +146 -0
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/databases.d.ts +5 -5
- 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 +26 -1
- package/dist/server/services/index.d.ts.map +1 -1
- package/dist/server/services/index.js +60 -1
- package/dist/server/services/index.js.map +1 -1
- package/dist/server/services/versions.d.ts +1 -1
- package/migrations/0010_transfers.sql +154 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +1 -1
- 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/SidebarSpaces.svelte +79 -0
- package/src/client/i18n.ts +614 -0
- package/src/client/index.ts +32 -0
- package/src/client/mock.ts +318 -0
- package/src/client/module.ts +36 -0
- package/src/client/pages/PageView.svelte +36 -0
- package/src/client/pages/TransfersPage.svelte +570 -0
- package/src/client/permissions.ts +11 -0
- package/src/client/query.ts +23 -0
- package/src/client/transfers.ts +142 -0
- package/src/contract/models.ts +152 -0
- package/src/contract/permissions.ts +84 -0
- package/src/contract/router.ts +147 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/services/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/services/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAEpD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAE7C,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAA;IACtC,MAAM,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAA;IACtC,KAAK,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;IACpC,QAAQ,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAA;IAC1C,QAAQ,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAA;IAC1C,SAAS,EAAE,UAAU,CAAC,OAAO,cAAc,CAAC,CAAA;IAC5C,YAAY,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;IAClD,YAAY,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAA;IAClD,OAAO,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAA;IACvC,OAAO,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAA;CACxC;AAID,oGAAoG;AACpG,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAsB3D;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAiCrE,CAAA;AAED,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,uFAAuF;AACvF,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { quireAccess } from './access.js';
|
|
2
3
|
import { quireComments } from './comments.js';
|
|
3
4
|
import { quireDatabases } from './databases.js';
|
|
5
|
+
import { quireExport } from './export.js';
|
|
6
|
+
import { quireImport } from './import.js';
|
|
4
7
|
import { quireOrganisation } from './organisation.js';
|
|
5
8
|
import { quirePages } from './pages.js';
|
|
6
9
|
import { quirePublications } from './publications.js';
|
|
@@ -14,22 +17,78 @@ export function quireServices(kernel) {
|
|
|
14
17
|
return existing;
|
|
15
18
|
const access = quireAccess(kernel);
|
|
16
19
|
const versions = quireVersions(kernel, access);
|
|
20
|
+
// An import writes pages *and* databases, so it is the one service built on another one: it needs
|
|
21
|
+
// `databases` to mint the properties whose keys its rows are keyed by. See `import.ts`'s `write`.
|
|
22
|
+
const databases = quireDatabases(kernel, access);
|
|
17
23
|
const services = {
|
|
18
24
|
access,
|
|
19
25
|
spaces: quireSpaces(access),
|
|
20
26
|
pages: quirePages(access),
|
|
21
27
|
versions,
|
|
22
28
|
comments: quireComments(access),
|
|
23
|
-
databases
|
|
29
|
+
databases,
|
|
24
30
|
organisation: quireOrganisation(access),
|
|
25
31
|
publications: quirePublications(kernel, access, versions),
|
|
32
|
+
exports: quireExport(kernel, access),
|
|
33
|
+
imports: quireImport(kernel, access, databases),
|
|
26
34
|
};
|
|
27
35
|
cache.set(kernel, services);
|
|
28
36
|
return services;
|
|
29
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The background work this module hands to a worker process, gathered here rather than in
|
|
40
|
+
* `index.ts`.
|
|
41
|
+
*
|
|
42
|
+
* One line in the module definition, so adding a job is an edit to this file and not to the file
|
|
43
|
+
* every other part of the module is also editing. The names reach pg-boss as `quire.<name>` — the
|
|
44
|
+
* kernel prefixes them — and only a process started with a worker role runs them; an API process
|
|
45
|
+
* registers nothing and merely sends.
|
|
46
|
+
*
|
|
47
|
+
* `retryLimit` is deliberately small. An export that fails for a reason of its own — Gotenberg is
|
|
48
|
+
* not running, a page will not render — records that reason on its row and returns *successfully*,
|
|
49
|
+
* because it has finished: it failed, and it said so, and repeating it would only write the same
|
|
50
|
+
* sentence three more times. A throw out of the handler means something underneath went wrong
|
|
51
|
+
* (the database, the broker), which is the case a retry is for.
|
|
52
|
+
*/
|
|
53
|
+
export const quireJobs = [
|
|
54
|
+
{
|
|
55
|
+
name: 'export',
|
|
56
|
+
schema: z.object({ workspaceId: z.uuid(), jobId: z.uuid() }),
|
|
57
|
+
handler: async (input, { kernel }) => {
|
|
58
|
+
await quireServices(kernel).exports.run(input.workspaceId, input.jobId);
|
|
59
|
+
},
|
|
60
|
+
options: { retryLimit: 2, retryDelay: 30, expireInSeconds: 900 },
|
|
61
|
+
},
|
|
62
|
+
/**
|
|
63
|
+
* An import, with **no retries at all** — the one place in this module where that is the safe
|
|
64
|
+
* setting rather than the timid one.
|
|
65
|
+
*
|
|
66
|
+
* `run` is idempotent against its own row (`done` and `failed` are terminal, and it claims the row
|
|
67
|
+
* before it does anything), so a retry after a *completed* run is a no-op. What a retry cannot be
|
|
68
|
+
* safe against is the case pg-boss actually retries: a handler that threw. An import commits its
|
|
69
|
+
* pages and its finished row in one transaction, so a throw means nothing was written — but a
|
|
70
|
+
* *lost connection after the commit* looks exactly the same from here, and running it again would
|
|
71
|
+
* write every page in the archive a second time. Two hundred duplicate pages in somebody's space
|
|
72
|
+
* is a worse outcome than a job that says it failed and can be started again by a person who can
|
|
73
|
+
* see what is in the space.
|
|
74
|
+
*
|
|
75
|
+
* The window is longer than an export's for the obvious reason: reading a few hundred megabytes of
|
|
76
|
+
* zip and writing four thousand rows is not a fifteen-minute job on a busy instance.
|
|
77
|
+
*/
|
|
78
|
+
{
|
|
79
|
+
name: 'import',
|
|
80
|
+
schema: z.object({ workspaceId: z.uuid(), jobId: z.uuid() }),
|
|
81
|
+
handler: async (input, { kernel }) => {
|
|
82
|
+
await quireServices(kernel).imports.run(input.workspaceId, input.jobId);
|
|
83
|
+
},
|
|
84
|
+
options: { retryLimit: 0, expireInSeconds: 3600 },
|
|
85
|
+
},
|
|
86
|
+
];
|
|
30
87
|
export * from './access.js';
|
|
31
88
|
export * from './comments.js';
|
|
32
89
|
export * from './databases.js';
|
|
90
|
+
export * from './export.js';
|
|
91
|
+
export * from './import.js';
|
|
33
92
|
export * from './organisation.js';
|
|
34
93
|
export * from './pages.js';
|
|
35
94
|
export * from './publications.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/services/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/services/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -76,9 +76,9 @@ export declare function quireVersions(kernel: Kernel, access: QuireAccess): {
|
|
|
76
76
|
kind: string;
|
|
77
77
|
size: number;
|
|
78
78
|
authorId: string | null;
|
|
79
|
-
text: string;
|
|
80
79
|
html: string | null;
|
|
81
80
|
state: Buffer<ArrayBufferLike>;
|
|
81
|
+
text: string;
|
|
82
82
|
snapshot: Buffer<ArrayBufferLike> | null;
|
|
83
83
|
}>;
|
|
84
84
|
/** What a reader is served, once somebody decides it is ready. */
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
-- Getting work in and out: a request to export a page, a subtree or a space, and a request to import
|
|
2
|
+
-- a Notion, Confluence or Markdown export into a space.
|
|
3
|
+
--
|
|
4
|
+
-- Hand-written rather than generated, like 0001, 0006 and 0008, because drizzle-kit writes neither
|
|
5
|
+
-- the policy nor the guards. Every statement is idempotent: `create table`, `create index` and
|
|
6
|
+
-- `create policy` throw on a replay, and a module migration that throws takes down the **whole host
|
|
7
|
+
-- service** rather than its own module — `core` hosts five. Drizzle keys applied migrations by
|
|
8
|
+
-- content hash, so regenerating the journal replays every file against a schema that already has its
|
|
9
|
+
-- objects.
|
|
10
|
+
--
|
|
11
|
+
--
|
|
12
|
+
-- WHY THESE ARE ROWS AND NOT REQUESTS
|
|
13
|
+
--
|
|
14
|
+
-- The work is unbounded on both sides. A space is a tree of arbitrary size; PDF means a round trip to
|
|
15
|
+
-- Gotenberg per page; a Notion export is a zip with thousands of files in it. A job that outlives its
|
|
16
|
+
-- HTTP request is what lets progress be reported, a failure be read afterwards by somebody who was
|
|
17
|
+
-- not watching, and a retry happen without the browser having stayed open. That is the whole reason
|
|
18
|
+
-- `state`, `counts`, `error` and `finished_at` are columns rather than fields of a response.
|
|
19
|
+
--
|
|
20
|
+
--
|
|
21
|
+
-- WHAT THESE TABLES DO NOT DO
|
|
22
|
+
--
|
|
23
|
+
-- Neither of them is a permission. "You may not export a page you may not read" and "an import writes
|
|
24
|
+
-- only into a space you may write to" are decided by the procedure, against the same space bindings
|
|
25
|
+
-- every other read in this module goes through, before a row is written at all. The policy below is
|
|
26
|
+
-- the tenant fence and nothing more — it stops workspace B seeing workspace A's jobs, and it has
|
|
27
|
+
-- nothing to say about which pages within one workspace went into an artefact. Do not read a row's
|
|
28
|
+
-- existence as evidence that anybody was allowed to ask for it.
|
|
29
|
+
CREATE TABLE IF NOT EXISTS "mod_quire"."export_jobs" (
|
|
30
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
31
|
+
"workspace_id" uuid NOT NULL,
|
|
32
|
+
-- Not null, where every other `created_by` in this schema is nullable. The artefact is the hazard:
|
|
33
|
+
-- a subtree export flattens pages with different readerships into one file, so whoever may fetch
|
|
34
|
+
-- it may read everything that went into it. Fencing the download to the person who asked needs a
|
|
35
|
+
-- person to fence it to, and a row whose requester has gone null cannot be fenced.
|
|
36
|
+
"requested_by" uuid NOT NULL,
|
|
37
|
+
-- `page` | `subtree` | `space`
|
|
38
|
+
"scope" text NOT NULL,
|
|
39
|
+
-- the page for `page` and `subtree`, the space for `space`; `scope` says which
|
|
40
|
+
"target_id" uuid NOT NULL,
|
|
41
|
+
-- `markdown` | `html` | `docx` | `pdf`
|
|
42
|
+
"format" text NOT NULL,
|
|
43
|
+
-- `queued` | `running` | `done` | `failed`
|
|
44
|
+
"state" text DEFAULT 'queued' NOT NULL,
|
|
45
|
+
-- The artefact in storage: written when the job finishes, null until then. Null while running is
|
|
46
|
+
-- what keeps a half file from being offered — the uploader writes the bytes, then the job records
|
|
47
|
+
-- the id, so this never points at a file that is still arriving. An id is not a URL: the download
|
|
48
|
+
-- is a signed URL a procedure mints, never a storage key a client assembles. See 0009 for what
|
|
49
|
+
-- that rule cost the last time it was broken.
|
|
50
|
+
"file_id" uuid,
|
|
51
|
+
-- Diagnostic text, in the language of whatever failed. Not a user-facing string: what a screen
|
|
52
|
+
-- shows comes from a message key chosen from `state`, and this is what makes the same failure
|
|
53
|
+
-- answerable a week later.
|
|
54
|
+
"error" text,
|
|
55
|
+
-- `{ total, done, skipped, failed }`. `skipped` is load-bearing: a subtree export by somebody who
|
|
56
|
+
-- may not read one of its children leaves that child out, and the count is the difference between
|
|
57
|
+
-- an export that is missing pages and an export that says so.
|
|
58
|
+
"counts" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
59
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
60
|
+
-- null while `queued` or `running`; set once, on the move to `done` or `failed`
|
|
61
|
+
"finished_at" timestamp with time zone
|
|
62
|
+
);
|
|
63
|
+
--> statement-breakpoint
|
|
64
|
+
|
|
65
|
+
-- The same shape pointed the other way, with one addition that carries the weight of the feature.
|
|
66
|
+
--
|
|
67
|
+
-- `report` is jsonb rather than a table, and that is a decision rather than a shortcut.
|
|
68
|
+
--
|
|
69
|
+
-- It is written by one job and read whole, as one document, by one screen: the person who ran the
|
|
70
|
+
-- import looking at what happened to their import. Nothing joins to an entry, nothing updates an
|
|
71
|
+
-- entry after the fact, nothing holds a foreign key into one, and no entry outlives its job. A
|
|
72
|
+
-- row-per-file table would be four thousand inserts to render a list only ever fetched by
|
|
73
|
+
-- `where job_id = $1`, plus a second tenant table with its own `workspace_id`, its own policy, its
|
|
74
|
+
-- own `TENANT_TABLES` entry and its own retention rule — all to store a document. Here, dropping the
|
|
75
|
+
-- job drops its report; there is nothing to sweep. The size is the part worth checking rather than
|
|
76
|
+
-- assuming: an entry is a path, one of three words, and either a uuid or a sentence, so a
|
|
77
|
+
-- five-thousand-file import is a few hundred kilobytes — one TOASTed value, read once.
|
|
78
|
+
--
|
|
79
|
+
-- **What would change if somebody wanted to query across imports** — "every file that failed for this
|
|
80
|
+
-- reason, across every import this month" — is that this stops being the right shape, and not by a
|
|
81
|
+
-- little. `jsonb_array_elements` over the whole table can answer it, but no index helps: a GIN index
|
|
82
|
+
-- on the column serves containment (`@>`), not a predicate plus an ordering inside the array. The
|
|
83
|
+
-- change is a `mod_quire.import_entries` table — `job_id`, `workspace_id`, `path`, `outcome`,
|
|
84
|
+
-- `page_id`, `reason` — indexed on `(workspace_id, outcome, created_at)`, written by the same worker,
|
|
85
|
+
-- with the existing rows backfilled by expanding this column through `jsonb_to_recordset`. It is a
|
|
86
|
+
-- tenant table, so it arrives with the full triple and a `TENANT_TABLES` entry, and it needs the
|
|
87
|
+
-- retention rule the column gets for free. The threshold is exactly that question: while every read
|
|
88
|
+
-- names one job, the column is right.
|
|
89
|
+
CREATE TABLE IF NOT EXISTS "mod_quire"."import_jobs" (
|
|
90
|
+
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
91
|
+
"workspace_id" uuid NOT NULL,
|
|
92
|
+
-- not null for the same reason as `export_jobs.requested_by`
|
|
93
|
+
"requested_by" uuid NOT NULL,
|
|
94
|
+
-- `notion` | `confluence` | `markdown`
|
|
95
|
+
"source" text NOT NULL,
|
|
96
|
+
-- the space being written into. An import always targets one space; there is no `scope` here.
|
|
97
|
+
"target_id" uuid NOT NULL,
|
|
98
|
+
-- The uploaded archive, and **not** called `file_id` on purpose. `export_jobs.file_id` is the file
|
|
99
|
+
-- a job produces and is null until it succeeds; this is the file a job consumes and exists before
|
|
100
|
+
-- the job does. One name pointing in two directions across two tables read by one screen is how a
|
|
101
|
+
-- worker writes its output id over the pointer to its input — losing the archive, so the job
|
|
102
|
+
-- cannot be retried and nothing reports that anything went missing.
|
|
103
|
+
"source_file_id" uuid NOT NULL,
|
|
104
|
+
-- `queued` | `running` | `done` | `failed`
|
|
105
|
+
"state" text DEFAULT 'queued' NOT NULL,
|
|
106
|
+
-- why the *job* failed; why one *file* failed is that file's entry in `report`
|
|
107
|
+
"error" text,
|
|
108
|
+
-- `{ total, done, skipped, failed }` — the same counters as an export, over files rather than pages
|
|
109
|
+
"counts" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
110
|
+
-- one entry per file: `{ path, outcome, pageId, reason }`, where outcome is imported|skipped|failed
|
|
111
|
+
"report" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
112
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
113
|
+
-- null while `queued` or `running`; set once, on the move to `done` or `failed`
|
|
114
|
+
"finished_at" timestamp with time zone
|
|
115
|
+
);
|
|
116
|
+
--> statement-breakpoint
|
|
117
|
+
|
|
118
|
+
-- Newest first, because that is the only order a list of jobs is ever read in — the one you just
|
|
119
|
+
-- started is the one you are looking for. `DESC NULLS LAST` rather than a plain index scanned
|
|
120
|
+
-- backwards so the ordering is the index's own, matching `recent_views_ws_user_idx` in 0007.
|
|
121
|
+
CREATE INDEX IF NOT EXISTS "export_jobs_ws_created_idx" ON "mod_quire"."export_jobs" USING btree ("workspace_id","created_at" DESC NULLS LAST);--> statement-breakpoint
|
|
122
|
+
|
|
123
|
+
-- "What is still running here", asked by the worker picking up work and by every client polling a
|
|
124
|
+
-- job it started. Leading with the state after the workspace keeps that probe off the rows of every
|
|
125
|
+
-- export the workspace has ever finished.
|
|
126
|
+
CREATE INDEX IF NOT EXISTS "export_jobs_ws_state_idx" ON "mod_quire"."export_jobs" USING btree ("workspace_id","state","created_at");--> statement-breakpoint
|
|
127
|
+
CREATE INDEX IF NOT EXISTS "import_jobs_ws_created_idx" ON "mod_quire"."import_jobs" USING btree ("workspace_id","created_at" DESC NULLS LAST);--> statement-breakpoint
|
|
128
|
+
CREATE INDEX IF NOT EXISTS "import_jobs_ws_state_idx" ON "mod_quire"."import_jobs" USING btree ("workspace_id","state","created_at");--> statement-breakpoint
|
|
129
|
+
|
|
130
|
+
-- Row-level security, the same triple every tenant table gets.
|
|
131
|
+
--
|
|
132
|
+
-- `force` matters: without it the table owner bypasses the policy, and the owner is the role the
|
|
133
|
+
-- service connects as. A *superuser* bypasses RLS whatever this says, and the development and CI
|
|
134
|
+
-- roles are superusers — so a test that does not connect as an unprivileged NOBYPASSRLS role proves
|
|
135
|
+
-- nothing about isolation.
|
|
136
|
+
--
|
|
137
|
+
-- Note what this does and does not fence, because these two tables invite the wrong reading. It is
|
|
138
|
+
-- the tenant boundary: workspace B cannot see workspace A's jobs, in either direction. It says
|
|
139
|
+
-- nothing about which *pages* went into an artefact, nothing about whether the requester was allowed
|
|
140
|
+
-- to ask, and nothing about who may download the result. Those are the procedure's job, decided
|
|
141
|
+
-- against the same space bindings as every other read in this module, before the row exists.
|
|
142
|
+
ALTER TABLE "mod_quire"."export_jobs" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
|
143
|
+
ALTER TABLE "mod_quire"."export_jobs" FORCE ROW LEVEL SECURITY;--> statement-breakpoint
|
|
144
|
+
DROP POLICY IF EXISTS "export_jobs_ws_isolation" ON "mod_quire"."export_jobs";--> statement-breakpoint
|
|
145
|
+
CREATE POLICY "export_jobs_ws_isolation" ON "mod_quire"."export_jobs"
|
|
146
|
+
USING (workspace_id::text = current_setting('app.workspace_id', true))
|
|
147
|
+
WITH CHECK (workspace_id::text = current_setting('app.workspace_id', true));--> statement-breakpoint
|
|
148
|
+
|
|
149
|
+
ALTER TABLE "mod_quire"."import_jobs" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
|
150
|
+
ALTER TABLE "mod_quire"."import_jobs" FORCE ROW LEVEL SECURITY;--> statement-breakpoint
|
|
151
|
+
DROP POLICY IF EXISTS "import_jobs_ws_isolation" ON "mod_quire"."import_jobs";--> statement-breakpoint
|
|
152
|
+
CREATE POLICY "import_jobs_ws_isolation" ON "mod_quire"."import_jobs"
|
|
153
|
+
USING (workspace_id::text = current_setting('app.workspace_id', true))
|
|
154
|
+
WITH CHECK (workspace_id::text = current_setting('app.workspace_id', true));
|
package/package.json
CHANGED