@starlove/orbit 1.4.0 → 2.0.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/Dockerfile +10 -8
- package/README.md +146 -164
- package/dist/full/csp.txt +1 -0
- package/dist/full/fonts/JetBrains_Mono/JetBrainsMono-subset.woff2 +0 -0
- package/dist/full/fonts/JetBrains_Mono/README.txt +5 -0
- package/dist/full/fonts/Mona_Sans/MonaSans-subset.woff2 +0 -0
- package/{public/fonts/Open_Sans → dist/full/fonts/Mona_Sans}/OFL.txt +93 -93
- package/dist/full/fonts/Mona_Sans/README.txt +10 -0
- package/{public/fonts/JetBrains_Mono → dist/full/fonts/Newsreader}/OFL.txt +93 -93
- package/dist/full/fonts/Newsreader/README.txt +8 -0
- package/dist/full/fonts/Newsreader/newsreader.woff2 +0 -0
- package/dist/full/index.html +36 -653
- package/dist/full/js/app.BmuponPJ.js +32 -0
- package/dist/full/js/app.BmuponPJ.js.map +1 -0
- package/dist/full/js/chunk.1fJGQGbW.js +2 -0
- package/dist/full/js/chunk.1fJGQGbW.js.map +1 -0
- package/dist/full/js/chunk.5D7JO2zp.js +2 -0
- package/dist/full/js/chunk.5D7JO2zp.js.map +1 -0
- package/dist/full/js/chunk.BrNn_D79.js +2 -0
- package/dist/full/js/chunk.BrNn_D79.js.map +1 -0
- package/dist/full/js/chunk.CokJXIqM.js +2 -0
- package/dist/full/js/chunk.CokJXIqM.js.map +1 -0
- package/dist/full/js/chunk.D_Haqy6R.js +2 -0
- package/dist/full/js/chunk.D_Haqy6R.js.map +1 -0
- package/dist/full/js/chunk.RO0jsH6I.js +15 -0
- package/dist/full/js/chunk.RO0jsH6I.js.map +1 -0
- package/dist/full/js/chunk.dqWOEtBM.js +2 -0
- package/dist/full/js/chunk.dqWOEtBM.js.map +1 -0
- package/dist/full/js/warm.f260bbbc.js +15 -0
- package/dist/full/orbits-favicon.png +0 -0
- package/dist/full/orbits-logo-bg-wide.png +0 -0
- package/dist/full/orbits-logo-bg.png +0 -0
- package/dist/full/orbits-logo.png +0 -0
- package/dist/full/styles.2b154ffe.css +1 -0
- package/dist/full/vendor/starlove-ui/theme-init.js +66 -0
- package/dist/full/version.json +2 -2
- package/docs/AGENT_GUIDE.md +114 -0
- package/docs/UPDATING.md +249 -64
- package/lib/adapters/indexeddb/blobs.d.ts +3 -0
- package/lib/adapters/indexeddb/blobs.js +54 -0
- package/lib/adapters/indexeddb/database.d.ts +60 -0
- package/lib/adapters/indexeddb/database.js +126 -0
- package/lib/adapters/indexeddb/index.d.ts +5 -0
- package/lib/adapters/indexeddb/index.js +3 -0
- package/lib/adapters/indexeddb/locks.d.ts +14 -0
- package/lib/adapters/indexeddb/locks.js +40 -0
- package/lib/adapters/indexeddb/store.d.ts +40 -0
- package/lib/adapters/indexeddb/store.js +424 -0
- package/lib/adapters/indexeddb/tables.d.ts +43 -0
- package/lib/adapters/indexeddb/tables.js +127 -0
- package/lib/adapters/memory/blobs.d.ts +3 -0
- package/lib/adapters/memory/blobs.js +44 -0
- package/lib/adapters/memory/board.d.ts +41 -0
- package/lib/adapters/memory/board.js +521 -0
- package/lib/adapters/memory/index.d.ts +2 -0
- package/lib/adapters/memory/index.js +2 -0
- package/lib/adapters/memory/search.d.ts +6 -0
- package/lib/adapters/memory/search.js +30 -0
- package/lib/adapters/memory/store.d.ts +34 -0
- package/lib/adapters/memory/store.js +266 -0
- package/lib/adapters/sqlite/backups.d.ts +53 -0
- package/lib/adapters/sqlite/backups.js +232 -0
- package/lib/adapters/sqlite/blobs.d.ts +14 -0
- package/lib/adapters/sqlite/blobs.js +109 -0
- package/lib/adapters/sqlite/board.d.ts +77 -0
- package/lib/adapters/sqlite/board.js +328 -0
- package/lib/adapters/sqlite/database.d.ts +67 -0
- package/lib/adapters/sqlite/database.js +285 -0
- package/lib/adapters/sqlite/index.d.ts +3 -0
- package/lib/adapters/sqlite/index.js +2 -0
- package/lib/adapters/sqlite/lock.d.ts +11 -0
- package/lib/adapters/sqlite/lock.js +34 -0
- package/lib/adapters/sqlite/records.d.ts +54 -0
- package/lib/adapters/sqlite/records.js +546 -0
- package/lib/adapters/sqlite/schema.d.ts +54 -0
- package/lib/adapters/sqlite/schema.js +276 -0
- package/lib/adapters/sqlite/store.d.ts +39 -0
- package/lib/adapters/sqlite/store.js +385 -0
- package/lib/browser/in-page.d.ts +31 -0
- package/lib/browser/in-page.js +46 -0
- package/lib/browser/old-preview.d.ts +13 -0
- package/lib/browser/old-preview.js +80 -0
- package/lib/browser/preview-board.d.ts +8 -0
- package/lib/browser/preview-board.js +69 -0
- package/lib/cli/main.d.ts +7 -0
- package/lib/cli/main.js +343 -0
- package/lib/cli/orbit.d.ts +8 -0
- package/lib/cli/orbit.js +25 -0
- package/lib/client/http.d.ts +38 -0
- package/lib/client/http.js +334 -0
- package/lib/client/local.d.ts +26 -0
- package/lib/client/local.js +190 -0
- package/lib/client/object-url.d.ts +8 -0
- package/lib/client/object-url.js +21 -0
- package/lib/client/types.d.ts +268 -0
- package/lib/client/types.js +8 -0
- package/lib/commands/agent-tools.d.ts +31 -0
- package/lib/commands/agent-tools.js +100 -0
- package/lib/commands/index.d.ts +56 -0
- package/lib/commands/index.js +298 -0
- package/lib/commands/parse.d.ts +73 -0
- package/lib/commands/parse.js +231 -0
- package/lib/commands/present.d.ts +134 -0
- package/lib/commands/present.js +198 -0
- package/lib/commands/session.d.ts +85 -0
- package/lib/commands/session.js +138 -0
- package/lib/commands/verbs.d.ts +35 -0
- package/lib/commands/verbs.js +842 -0
- package/lib/core/actors.d.ts +18 -0
- package/lib/core/actors.js +45 -0
- package/lib/core/agent.d.ts +128 -0
- package/lib/core/agent.js +323 -0
- package/lib/core/boards.d.ts +113 -0
- package/lib/core/boards.js +288 -0
- package/lib/core/bootstrap.d.ts +47 -0
- package/lib/core/bootstrap.js +100 -0
- package/lib/core/bytes.d.ts +10 -0
- package/lib/core/bytes.js +52 -0
- package/lib/core/card-paths.d.ts +25 -0
- package/lib/core/card-paths.js +44 -0
- package/lib/core/context.d.ts +60 -0
- package/lib/core/context.js +42 -0
- package/lib/core/cross-board.d.ts +81 -0
- package/lib/core/cross-board.js +228 -0
- package/lib/core/describe.d.ts +21 -0
- package/lib/core/describe.js +81 -0
- package/lib/core/errors.d.ts +159 -0
- package/lib/core/errors.js +160 -0
- package/lib/core/files.d.ts +79 -0
- package/lib/core/files.js +262 -0
- package/lib/core/history.d.ts +20 -0
- package/lib/core/history.js +53 -0
- package/lib/core/image-types.d.ts +16 -0
- package/lib/core/image-types.js +36 -0
- package/lib/core/model.d.ts +263 -0
- package/lib/core/model.js +34 -0
- package/lib/core/nebula-link.d.ts +14 -0
- package/lib/core/nebula-link.js +39 -0
- package/lib/core/relations.d.ts +58 -0
- package/lib/core/relations.js +112 -0
- package/lib/core/search.d.ts +38 -0
- package/lib/core/search.js +92 -0
- package/lib/core/sessions.d.ts +44 -0
- package/lib/core/sessions.js +158 -0
- package/lib/core/snapshot-schema.d.ts +295 -0
- package/lib/core/snapshot-schema.js +82 -0
- package/lib/core/snapshots.d.ts +75 -0
- package/lib/core/snapshots.js +409 -0
- package/lib/core/states.d.ts +25 -0
- package/lib/core/states.js +126 -0
- package/lib/core/text.d.ts +40 -0
- package/lib/core/text.js +113 -0
- package/lib/core/tickets.d.ts +69 -0
- package/lib/core/tickets.js +311 -0
- package/lib/core/trello.d.ts +17 -0
- package/lib/core/trello.js +269 -0
- package/lib/core/verdicts.d.ts +28 -0
- package/lib/core/verdicts.js +92 -0
- package/lib/core/views.d.ts +95 -0
- package/lib/core/views.js +186 -0
- package/lib/http/app.d.ts +46 -0
- package/lib/http/app.js +491 -0
- package/lib/mcp-server.d.ts +10 -0
- package/lib/mcp-server.js +120 -0
- package/lib/node/assets.d.ts +7 -0
- package/lib/node/assets.js +68 -0
- package/lib/node/data-folder.d.ts +21 -0
- package/lib/node/data-folder.js +34 -0
- package/lib/node/env.d.ts +38 -0
- package/lib/node/env.js +49 -0
- package/lib/node/host-check.d.ts +24 -0
- package/lib/node/host-check.js +53 -0
- package/lib/node/host-services.d.ts +9 -0
- package/lib/node/host-services.js +150 -0
- package/lib/node/local.d.ts +22 -0
- package/lib/node/local.js +61 -0
- package/lib/node/migrate.d.ts +53 -0
- package/lib/node/migrate.js +635 -0
- package/lib/node/package.d.ts +7 -0
- package/lib/node/package.js +14 -0
- package/lib/node/repo-board.d.ts +49 -0
- package/lib/node/repo-board.js +245 -0
- package/lib/node/serve.d.ts +8 -0
- package/lib/node/serve.js +106 -0
- package/lib/orbit.d.ts +47 -0
- package/lib/orbit.js +44 -0
- package/lib/quiet-sqlite-warning.d.ts +1 -0
- package/lib/quiet-sqlite-warning.js +18 -0
- package/lib/store/blob-path.d.ts +6 -0
- package/lib/store/blob-path.js +12 -0
- package/lib/store/conformance.d.ts +52 -0
- package/lib/store/conformance.js +1449 -0
- package/lib/store/search-words.d.ts +10 -0
- package/lib/store/search-words.js +19 -0
- package/lib/store/types.d.ts +452 -0
- package/lib/store/types.js +34 -0
- package/llms.txt +63 -0
- package/package.json +109 -25
- package/src/cli/orbit.js +11 -658
- package/src/mcp-server.js +16 -876
- package/SKILL-ORBIT.md +0 -151
- package/build.mjs +0 -210
- package/dist/full/js/app.62905fcb.js +0 -952
- package/dist/full/js/app.62905fcb.js.map +0 -7
- package/dist/full/styles.4e60d824.css +0 -1
- package/docs/AGENT_PROTOCOL.md +0 -424
- package/docs/MCP_SETUP.md +0 -54
- package/docs/ORBIT_DISPATCH.md +0 -280
- package/docs/SS-Orbit-Dark.png +0 -0
- package/docs/SS-Orbit-Light.png +0 -0
- package/docs/mcp-client.example.json +0 -13
- package/docs/setup/orbit-init-wizard.md +0 -174
- package/public/apple-touch-icon.png +0 -0
- package/public/cursors/live_arrow.cur +0 -0
- package/public/cursors/live_link.cur +0 -0
- package/public/cursors/live_move.cur +0 -0
- package/public/cursors/live_select.cur +0 -0
- package/public/cursors/live_unavail.cur +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Bold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-BoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Italic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Light.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-LightItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Medium.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-MediumItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-Regular.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBold.ttf +0 -0
- package/public/fonts/Chakra_Petch/ChakraPetch-SemiBoldItalic.ttf +0 -0
- package/public/fonts/Chakra_Petch/OFL.txt +0 -93
- package/public/fonts/Inter/Inter-Italic-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/Inter-VariableFont_opsz,wght.ttf +0 -0
- package/public/fonts/Inter/OFL.txt +0 -93
- package/public/fonts/Inter/README.txt +0 -118
- package/public/fonts/JetBrains_Mono/JetBrainsMono-Italic-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/JetBrainsMono-VariableFont_wght.ttf +0 -0
- package/public/fonts/JetBrains_Mono/README.txt +0 -79
- package/public/fonts/OpenRing-Regular.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- package/public/fonts/Open_Sans/README.txt +0 -100
- package/public/fonts/Orbitron/OFL.txt +0 -93
- package/public/fonts/Orbitron/Orbitron-VariableFont_wght.ttf +0 -0
- package/public/fonts/Orbitron/README.txt +0 -68
- package/public/fonts/Rajdhani/OFL.txt +0 -93
- package/public/fonts/Rajdhani/Rajdhani-Bold.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Light.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Medium.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-Regular.ttf +0 -0
- package/public/fonts/Rajdhani/Rajdhani-SemiBold.ttf +0 -0
- package/public/fonts/runescape-uf.ttf +0 -0
- package/public/index.html +0 -769
- package/public/js/actor-labels.js +0 -65
- package/public/js/api.js +0 -39
- package/public/js/app.js +0 -45
- package/public/js/artifact-drawer.js +0 -79
- package/public/js/board-export.js +0 -161
- package/public/js/board-menu.js +0 -374
- package/public/js/card-actions.js +0 -545
- package/public/js/config.js +0 -20
- package/public/js/create-card.js +0 -190
- package/public/js/dom.js +0 -19
- package/public/js/drawer.js +0 -273
- package/public/js/font-preference.js +0 -229
- package/public/js/format.js +0 -433
- package/public/js/help-menu.js +0 -31
- package/public/js/icons.js +0 -16
- package/public/js/kanban.js +0 -694
- package/public/js/local-backend.js +0 -1015
- package/public/js/main.js +0 -161
- package/public/js/modal.js +0 -33
- package/public/js/motion-preference.js +0 -64
- package/public/js/panel.js +0 -58
- package/public/js/router.js +0 -134
- package/public/js/search.js +0 -56
- package/public/js/settings-tabs.js +0 -7
- package/public/js/settings.js +0 -1634
- package/public/js/sse.js +0 -159
- package/public/js/starscape.js +0 -332
- package/public/js/state.js +0 -129
- package/public/js/text-editing.js +0 -128
- package/public/js/theme-preference.js +0 -95
- package/public/js/ticket-detail.js +0 -1516
- package/public/js/ticket-typeahead.js +0 -49
- package/public/js/toast.js +0 -89
- package/public/js/topbar-notes.js +0 -155
- package/public/js/transport.js +0 -22
- package/public/js/typeahead.js +0 -206
- package/public/js/unread.js +0 -121
- package/public/js/update-check.js +0 -58
- package/public/js/url-routes.js +0 -94
- package/public/orbits-favicon.png +0 -0
- package/public/orbits-logo-bg-wide.png +0 -0
- package/public/orbits-logo-bg.png +0 -0
- package/public/orbits-logo.png +0 -0
- package/public/styles.css +0 -7709
- package/src/core/agent.js +0 -387
- package/src/core/agents-md.js +0 -109
- package/src/core/artifacts.js +0 -149
- package/src/core/attachments.js +0 -238
- package/src/core/auth.js +0 -85
- package/src/core/backups.js +0 -142
- package/src/core/board.js +0 -148
- package/src/core/boards.js +0 -409
- package/src/core/bootstrap.js +0 -170
- package/src/core/css-bundle.js +0 -30
- package/src/core/db.js +0 -315
- package/src/core/delete-board.js +0 -163
- package/src/core/dispatch.js +0 -1007
- package/src/core/events.js +0 -314
- package/src/core/http.js +0 -115
- package/src/core/migrate.js +0 -198
- package/src/core/paths.js +0 -55
- package/src/core/provision-repo-board.js +0 -119
- package/src/core/queries.js +0 -246
- package/src/core/registry.js +0 -211
- package/src/core/relations.js +0 -163
- package/src/core/review-verdicts.js +0 -138
- package/src/core/router.js +0 -556
- package/src/core/search.js +0 -183
- package/src/core/seed.js +0 -234
- package/src/core/snapshots.js +0 -692
- package/src/core/states.js +0 -167
- package/src/core/system-picker.js +0 -109
- package/src/core/tickets.js +0 -400
- package/src/core/util.js +0 -106
- package/src/core/version-check.js +0 -154
- package/src/mcp/orbit-client.js +0 -287
- package/src/server.js +0 -115
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One board's records in SQL: the port's BoardReader and BoardWriter over a
|
|
3
|
+
* connection. Each method is one statement, prepared once per connection, and
|
|
4
|
+
* reads that serve a whole board return every row at once. Records come back
|
|
5
|
+
* built field for field in the order src/core/model.ts lists them, with the
|
|
6
|
+
* board's id added where a record names its board (board.db holds one board,
|
|
7
|
+
* so its rows don't repeat it).
|
|
8
|
+
*/
|
|
9
|
+
import { OrbitError } from "../../core/errors.js";
|
|
10
|
+
import { literalQuery, queryWords } from "../../store/search-words.js";
|
|
11
|
+
function kind(table, fields) {
|
|
12
|
+
const columns = fields.filter((field) => field !== "board_id");
|
|
13
|
+
return {
|
|
14
|
+
table,
|
|
15
|
+
fields,
|
|
16
|
+
columns,
|
|
17
|
+
select: `SELECT ${columns.join(", ")} FROM ${table}`,
|
|
18
|
+
insert: `INSERT INTO ${table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export const BOARD = kind("board", ["id", "slug", "name", "repo_url", "default_branch", "project_notes", "agent_instructions", "ai_enabled", "created_at", "updated_at"]);
|
|
22
|
+
const STATE = kind("states", ["id", "board_id", "name", "position", "is_default", "role", "created_at"]);
|
|
23
|
+
const LABEL = kind("labels", ["id", "board_id", "name", "color", "created_at"]);
|
|
24
|
+
const TICKET = kind("tickets", [
|
|
25
|
+
"id", "board_id", "number", "title", "description", "type", "parent_ticket_id", "ai_plan", "plan_artifact_path", "implementation_summary",
|
|
26
|
+
"implementation_artifact_path", "implementation_updates", "nebula_url", "state_id", "priority", "created_by", "created_at", "updated_at", "archived_at"
|
|
27
|
+
]);
|
|
28
|
+
const TICKET_LABEL = kind("ticket_labels", ["ticket_id", "label_id"]);
|
|
29
|
+
const COMMENT = kind("comments", ["id", "ticket_id", "author", "kind", "body", "omitted_for_ai", "created_at"]);
|
|
30
|
+
const RELATION = kind("relations", ["id", "source_ticket_id", "target_ticket_id", "type", "created_at"]);
|
|
31
|
+
export const CROSS = kind("cross_board_relations", ["id", "source_ticket_id", "target_board_id", "target_ticket_id", "type", "created_at"]);
|
|
32
|
+
const ENTRY = kind("board_entries", ["id", "board_id", "type", "title", "body", "ticket_id", "created_by", "created_at", "updated_at", "struck_at"]);
|
|
33
|
+
const VERDICT = kind("review_verdicts", [
|
|
34
|
+
"id", "ticket_id", "verdict", "blocking_findings_json", "optional_findings_json", "evidence_commands_json", "reviewer_profile",
|
|
35
|
+
"reviewer_session_id", "reviewed_commit_sha", "dispatch_run_id", "supersedes_prior_review_id", "created_by", "created_at"
|
|
36
|
+
]);
|
|
37
|
+
const ATTACHMENT = kind("attachments", ["id", "ticket_id", "board_id", "original_name", "stored_path", "mime_type", "size_bytes", "sha256", "created_by", "created_at"]);
|
|
38
|
+
export const EVENT = kind("events", ["id", "ticket_id", "actor", "type", "body_json", "created_at"]);
|
|
39
|
+
/** The record a row holds, built in the model's field order. */
|
|
40
|
+
export function toRecord(of, row, boardId) {
|
|
41
|
+
const record = {};
|
|
42
|
+
for (const field of of.fields)
|
|
43
|
+
record[field] = field === "board_id" ? boardId : row[field];
|
|
44
|
+
return record;
|
|
45
|
+
}
|
|
46
|
+
/** A copy of a record handed in, built the same way, so the caller's object is never kept. */
|
|
47
|
+
function copyOf(of, record, boardId) {
|
|
48
|
+
return toRecord(of, record, boardId);
|
|
49
|
+
}
|
|
50
|
+
/** The values of `columns` in `record`, in order, for a statement's parameters. */
|
|
51
|
+
function valuesOf(columns, record) {
|
|
52
|
+
return columns.map((column) => record[column]);
|
|
53
|
+
}
|
|
54
|
+
/** The fields of `patch` that are set and may change, in the order given. */
|
|
55
|
+
function patched(patch, allowed) {
|
|
56
|
+
return Object.entries(patch).filter(([key, value]) => value !== undefined && allowed.has(key));
|
|
57
|
+
}
|
|
58
|
+
const updatable = (of, fixed) => new Set(of.columns.filter((column) => !fixed.includes(column)));
|
|
59
|
+
const TICKET_PATCH = updatable(TICKET, ["id", "number", "created_by", "created_at"]);
|
|
60
|
+
const STATE_PATCH = new Set(["name", "position", "is_default", "role"]);
|
|
61
|
+
const ENTRY_PATCH = new Set(["struck_at", "updated_at"]);
|
|
62
|
+
const BOARD_PATCH = updatable(BOARD, ["id", "created_at"]);
|
|
63
|
+
/** A new card's columns: all but its number, which the insert computes. */
|
|
64
|
+
const NEW_TICKET = TICKET.columns.filter((column) => column !== "number");
|
|
65
|
+
const json = (ids) => JSON.stringify(ids);
|
|
66
|
+
const IN_IDS = "(SELECT value FROM json_each(?))";
|
|
67
|
+
const BY_TIME = "ORDER BY created_at, seq";
|
|
68
|
+
// Statements whose text never changes. Each binds plain `?` in order, a
|
|
69
|
+
// value repeated where it is used twice: Node 22.16 binds an array of values to
|
|
70
|
+
// `?1`-style parameters by occurrence, and refuses one named twice.
|
|
71
|
+
const SQL = {
|
|
72
|
+
board: `${BOARD.select} LIMIT 1`,
|
|
73
|
+
setBoard: BOARD.insert.replace("INSERT INTO", "INSERT OR REPLACE INTO"),
|
|
74
|
+
states: `${STATE.select} ORDER BY position, seq`,
|
|
75
|
+
state: `${STATE.select} WHERE id = ?`,
|
|
76
|
+
labels: `${LABEL.select} ORDER BY name`,
|
|
77
|
+
ticket: `${TICKET.select} WHERE id = ?`,
|
|
78
|
+
ticketByNumber: `${TICKET.select} WHERE number = ?`,
|
|
79
|
+
ticketByTitle: `${TICKET.select} WHERE lower(title) = lower(?) ORDER BY archived_at IS NOT NULL, number DESC LIMIT 1`,
|
|
80
|
+
lastNumber: "SELECT coalesce(max(number), 0) AS number FROM tickets",
|
|
81
|
+
createTicket: `INSERT INTO tickets (number, ${NEW_TICKET.join(", ")}) ` +
|
|
82
|
+
`VALUES ((SELECT coalesce(max(number), 0) + 1 FROM tickets), ${NEW_TICKET.map(() => "?").join(", ")}) RETURNING number`,
|
|
83
|
+
ticketExists: "SELECT 1 AS here FROM tickets WHERE id = ?",
|
|
84
|
+
removeTicket: "DELETE FROM tickets WHERE id = ?",
|
|
85
|
+
ticketLabels: `${TICKET_LABEL.select} ORDER BY seq`,
|
|
86
|
+
ticketLabelsOf: `${TICKET_LABEL.select} WHERE ticket_id IN ${IN_IDS} ORDER BY seq`,
|
|
87
|
+
unlinkLabels: "DELETE FROM ticket_labels WHERE ticket_id = ?",
|
|
88
|
+
linkLabel: "INSERT INTO ticket_labels (ticket_id, label_id) VALUES (?, ?) ON CONFLICT (ticket_id, label_id) DO NOTHING",
|
|
89
|
+
comment: `${COMMENT.select} WHERE id = ?`,
|
|
90
|
+
comments: `${COMMENT.select} ${BY_TIME}`,
|
|
91
|
+
commentsOf: `${COMMENT.select} WHERE ticket_id = ? ${BY_TIME}`,
|
|
92
|
+
commentCounts: "SELECT ticket_id, count(*) AS count FROM comments WHERE (? = 0 OR omitted_for_ai = 0) AND (? IS NULL OR body <> ?) GROUP BY ticket_id ORDER BY min(seq)",
|
|
93
|
+
hideComment: `UPDATE comments SET omitted_for_ai = ? WHERE id = ? RETURNING ${COMMENT.columns.join(", ")}`,
|
|
94
|
+
relation: `${RELATION.select} WHERE id = ?`,
|
|
95
|
+
relations: `${RELATION.select} ${BY_TIME}`,
|
|
96
|
+
relationsOf: `${RELATION.select} WHERE source_ticket_id = ? OR target_ticket_id = ? ${BY_TIME}`,
|
|
97
|
+
removeRelation: "DELETE FROM relations WHERE id = ?",
|
|
98
|
+
crossRelations: `${CROSS.select} ${BY_TIME}`,
|
|
99
|
+
crossRelationsOf: `${CROSS.select} WHERE source_ticket_id = ? ${BY_TIME}`,
|
|
100
|
+
removeCrossRelation: "DELETE FROM cross_board_relations WHERE id = ?",
|
|
101
|
+
entry: `${ENTRY.select} WHERE id = ?`,
|
|
102
|
+
entries: `${ENTRY.select} ${BY_TIME}`,
|
|
103
|
+
removeEntry: "DELETE FROM board_entries WHERE id = ?",
|
|
104
|
+
verdict: `${VERDICT.select} WHERE id = ?`,
|
|
105
|
+
verdicts: `${VERDICT.select} ${BY_TIME}`,
|
|
106
|
+
verdictsOf: `${VERDICT.select} WHERE ticket_id = ? ${BY_TIME}`,
|
|
107
|
+
attachment: `${ATTACHMENT.select} WHERE id = ?`,
|
|
108
|
+
attachments: `${ATTACHMENT.select} ${BY_TIME}`,
|
|
109
|
+
attachmentsOf: `${ATTACHMENT.select} WHERE ticket_id = ? ${BY_TIME}`,
|
|
110
|
+
removeAttachment: "DELETE FROM attachments WHERE id = ?",
|
|
111
|
+
latestEvents: `SELECT ${EVENT.columns.join(", ")} FROM (SELECT *, row_number() OVER (PARTITION BY ticket_id ORDER BY created_at DESC, seq DESC) AS newest ` +
|
|
112
|
+
`FROM events WHERE ticket_id IN ${IN_IDS} AND type IN ${IN_IDS}) WHERE newest = 1 ${BY_TIME}`,
|
|
113
|
+
eventsAfter: `${EVENT.select} WHERE seq > (SELECT seq FROM events WHERE id = ?) ORDER BY seq LIMIT ?`,
|
|
114
|
+
lastEvent: `${EVENT.select} ORDER BY seq DESC LIMIT 1`,
|
|
115
|
+
searchWords: "SELECT tickets.id AS ticket_id, bm25(search) AS rank FROM search JOIN tickets ON tickets.number = search.rowid " +
|
|
116
|
+
"WHERE search MATCH ? AND tickets.archived_at IS NULL ORDER BY rank, tickets.number LIMIT ?",
|
|
117
|
+
searchText: "SELECT tickets.id AS ticket_id, 0 AS rank FROM search JOIN tickets ON tickets.number = search.rowid WHERE tickets.archived_at IS NULL " +
|
|
118
|
+
"AND (search.title LIKE ? ESCAPE '\\' OR search.description LIKE ? ESCAPE '\\' OR search.details LIKE ? ESCAPE '\\' OR search.comments LIKE ? ESCAPE '\\') " +
|
|
119
|
+
"ORDER BY tickets.number LIMIT ?",
|
|
120
|
+
index: "INSERT OR REPLACE INTO search (rowid, title, description, type, details, comments) SELECT number, ?, ?, ?, ?, ? FROM tickets WHERE id = ?",
|
|
121
|
+
unindex: "DELETE FROM search WHERE rowid = (SELECT number FROM tickets WHERE id = ?)",
|
|
122
|
+
clear: [
|
|
123
|
+
"search", "ticket_labels", "relations", "cross_board_relations", "comments", "review_verdicts", "attachments", "board_entries", "events", "tickets", "labels", "states", "board"
|
|
124
|
+
].map((table) => `DELETE FROM ${table};`).join(" ")
|
|
125
|
+
};
|
|
126
|
+
/** For readers, which have nothing to check before a call. */
|
|
127
|
+
export const noWait = () => Promise.resolve();
|
|
128
|
+
/** The port's BoardReader over a connection. */
|
|
129
|
+
export function reader({ c, boardId, before }) {
|
|
130
|
+
const one = (of, sql, params) => {
|
|
131
|
+
const row = c.get(sql, params);
|
|
132
|
+
return row ? toRecord(of, row, boardId) : null;
|
|
133
|
+
};
|
|
134
|
+
const many = (of, sql, params = []) => c.all(sql, params).map((row) => toRecord(of, row, boardId));
|
|
135
|
+
return {
|
|
136
|
+
board: {
|
|
137
|
+
async get() {
|
|
138
|
+
await before();
|
|
139
|
+
const board = one(BOARD, SQL.board, []);
|
|
140
|
+
if (!board)
|
|
141
|
+
throw new OrbitError("board_not_found");
|
|
142
|
+
return board;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
states: {
|
|
146
|
+
async list() {
|
|
147
|
+
await before();
|
|
148
|
+
return many(STATE, SQL.states);
|
|
149
|
+
},
|
|
150
|
+
async get(id) {
|
|
151
|
+
await before();
|
|
152
|
+
return one(STATE, SQL.state, [id]);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
labels: {
|
|
156
|
+
async list() {
|
|
157
|
+
await before();
|
|
158
|
+
return many(LABEL, SQL.labels);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
tickets: {
|
|
162
|
+
async get(id) {
|
|
163
|
+
await before();
|
|
164
|
+
return one(TICKET, SQL.ticket, [id]);
|
|
165
|
+
},
|
|
166
|
+
async byNumber(number) {
|
|
167
|
+
await before();
|
|
168
|
+
return one(TICKET, SQL.ticketByNumber, [number]);
|
|
169
|
+
},
|
|
170
|
+
async byTitle(title) {
|
|
171
|
+
await before();
|
|
172
|
+
return one(TICKET, SQL.ticketByTitle, [title]);
|
|
173
|
+
},
|
|
174
|
+
async list(filter = {}) {
|
|
175
|
+
await before();
|
|
176
|
+
if (filter.ids?.length === 0)
|
|
177
|
+
return [];
|
|
178
|
+
const [sql, params] = ticketQuery(filter);
|
|
179
|
+
return many(TICKET, sql, params);
|
|
180
|
+
},
|
|
181
|
+
async lastNumber() {
|
|
182
|
+
await before();
|
|
183
|
+
return Number(c.get(SQL.lastNumber)?.number ?? 0);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
ticketLabels: {
|
|
187
|
+
async list(ticketIds) {
|
|
188
|
+
await before();
|
|
189
|
+
if (ticketIds === undefined)
|
|
190
|
+
return many(TICKET_LABEL, SQL.ticketLabels);
|
|
191
|
+
return ticketIds.length === 0 ? [] : many(TICKET_LABEL, SQL.ticketLabelsOf, [json(ticketIds)]);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
comments: {
|
|
195
|
+
async get(id) {
|
|
196
|
+
await before();
|
|
197
|
+
return one(COMMENT, SQL.comment, [id]);
|
|
198
|
+
},
|
|
199
|
+
async list(ticketId) {
|
|
200
|
+
await before();
|
|
201
|
+
return ticketId === undefined ? many(COMMENT, SQL.comments) : many(COMMENT, SQL.commentsOf, [ticketId]);
|
|
202
|
+
},
|
|
203
|
+
async countByTicket(options = {}) {
|
|
204
|
+
await before();
|
|
205
|
+
return c.all(SQL.commentCounts, [options.forAgents ? 1 : 0, options.exceptBody ?? null, options.exceptBody ?? null]).map((row) => ({ ticket_id: String(row.ticket_id), count: Number(row.count) }));
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
relations: {
|
|
209
|
+
async get(id) {
|
|
210
|
+
await before();
|
|
211
|
+
return one(RELATION, SQL.relation, [id]);
|
|
212
|
+
},
|
|
213
|
+
async list(ticketId) {
|
|
214
|
+
await before();
|
|
215
|
+
return ticketId === undefined ? many(RELATION, SQL.relations) : many(RELATION, SQL.relationsOf, [ticketId, ticketId]);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
crossBoardRelations: {
|
|
219
|
+
async list(ticketId) {
|
|
220
|
+
await before();
|
|
221
|
+
return ticketId === undefined ? many(CROSS, SQL.crossRelations) : many(CROSS, SQL.crossRelationsOf, [ticketId]);
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
entries: {
|
|
225
|
+
async get(id) {
|
|
226
|
+
await before();
|
|
227
|
+
return one(ENTRY, SQL.entry, [id]);
|
|
228
|
+
},
|
|
229
|
+
async list() {
|
|
230
|
+
await before();
|
|
231
|
+
return many(ENTRY, SQL.entries);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
verdicts: {
|
|
235
|
+
async get(id) {
|
|
236
|
+
await before();
|
|
237
|
+
return one(VERDICT, SQL.verdict, [id]);
|
|
238
|
+
},
|
|
239
|
+
async list(ticketId) {
|
|
240
|
+
await before();
|
|
241
|
+
return ticketId === undefined ? many(VERDICT, SQL.verdicts) : many(VERDICT, SQL.verdictsOf, [ticketId]);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
attachments: {
|
|
245
|
+
async get(id) {
|
|
246
|
+
await before();
|
|
247
|
+
return one(ATTACHMENT, SQL.attachment, [id]);
|
|
248
|
+
},
|
|
249
|
+
async list(ticketId) {
|
|
250
|
+
await before();
|
|
251
|
+
return ticketId === undefined ? many(ATTACHMENT, SQL.attachments) : many(ATTACHMENT, SQL.attachmentsOf, [ticketId]);
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
events: {
|
|
255
|
+
async list(filter = {}) {
|
|
256
|
+
await before();
|
|
257
|
+
if (filter.types?.length === 0)
|
|
258
|
+
return [];
|
|
259
|
+
const [sql, params] = eventQuery(filter);
|
|
260
|
+
return many(EVENT, sql, params);
|
|
261
|
+
},
|
|
262
|
+
async latest(ticketIds, types) {
|
|
263
|
+
await before();
|
|
264
|
+
if (ticketIds.length === 0 || types.length === 0)
|
|
265
|
+
return [];
|
|
266
|
+
return many(EVENT, SQL.latestEvents, [json(ticketIds), json(types)]);
|
|
267
|
+
},
|
|
268
|
+
async after(id, limit) {
|
|
269
|
+
await before();
|
|
270
|
+
return many(EVENT, SQL.eventsAfter, [id, limit]);
|
|
271
|
+
},
|
|
272
|
+
async last() {
|
|
273
|
+
await before();
|
|
274
|
+
return one(EVENT, SQL.lastEvent, []);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
search: {
|
|
278
|
+
async query(text, options) {
|
|
279
|
+
await before();
|
|
280
|
+
return search(c, text, options.limit);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function ticketQuery(filter) {
|
|
286
|
+
const where = [];
|
|
287
|
+
const params = [];
|
|
288
|
+
if (filter.archived !== undefined)
|
|
289
|
+
where.push(filter.archived ? "archived_at IS NOT NULL" : "archived_at IS NULL");
|
|
290
|
+
if (filter.parent !== undefined) {
|
|
291
|
+
where.push("parent_ticket_id = ?");
|
|
292
|
+
params.push(filter.parent);
|
|
293
|
+
}
|
|
294
|
+
if (filter.state !== undefined) {
|
|
295
|
+
where.push("state_id = ?");
|
|
296
|
+
params.push(filter.state);
|
|
297
|
+
}
|
|
298
|
+
if (filter.ids !== undefined) {
|
|
299
|
+
where.push(`id IN ${IN_IDS}`);
|
|
300
|
+
params.push(json(filter.ids));
|
|
301
|
+
}
|
|
302
|
+
return [`${TICKET.select}${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY number`, params];
|
|
303
|
+
}
|
|
304
|
+
function eventQuery(filter) {
|
|
305
|
+
const where = [];
|
|
306
|
+
const params = [];
|
|
307
|
+
if (filter.ticketId !== undefined) {
|
|
308
|
+
where.push("ticket_id = ?");
|
|
309
|
+
params.push(filter.ticketId);
|
|
310
|
+
}
|
|
311
|
+
if (filter.types !== undefined) {
|
|
312
|
+
where.push(`type IN ${IN_IDS}`);
|
|
313
|
+
params.push(json(filter.types));
|
|
314
|
+
}
|
|
315
|
+
if (filter.since !== undefined) {
|
|
316
|
+
where.push("created_at >= ?");
|
|
317
|
+
params.push(filter.since);
|
|
318
|
+
}
|
|
319
|
+
return [`${EVENT.select}${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ${BY_TIME}`, params];
|
|
320
|
+
}
|
|
321
|
+
/** The port's search rules over FTS5: every word a prefix, or the literal text with LIKE's wildcards escaped. */
|
|
322
|
+
function search(c, text, limit) {
|
|
323
|
+
const words = queryWords(text);
|
|
324
|
+
let rows;
|
|
325
|
+
if (words.length > 0) {
|
|
326
|
+
rows = c.all(SQL.searchWords, [words.map((word) => `"${word}"*`).join(" "), limit]);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
const literal = literalQuery(text);
|
|
330
|
+
if (literal === null)
|
|
331
|
+
return [];
|
|
332
|
+
const pattern = `%${literal.replace(/[\\%_]/g, "\\$&")}%`;
|
|
333
|
+
rows = c.all(SQL.searchText, [pattern, pattern, pattern, pattern, limit]);
|
|
334
|
+
}
|
|
335
|
+
return rows.map((row) => ({ ticket_id: String(row.ticket_id), rank: Number(row.rank) }));
|
|
336
|
+
}
|
|
337
|
+
/** The port's BoardWriter over a connection in an open transaction. */
|
|
338
|
+
export function writer(over, log, checks) {
|
|
339
|
+
const { c, boardId, before } = over;
|
|
340
|
+
const read = reader(over);
|
|
341
|
+
const onBoard = (id) => {
|
|
342
|
+
if (id !== boardId)
|
|
343
|
+
throw new OrbitError("invalid_reference");
|
|
344
|
+
};
|
|
345
|
+
const insert = (of, record) => {
|
|
346
|
+
c.run(of.insert, valuesOf(of.columns, record));
|
|
347
|
+
};
|
|
348
|
+
const removed = (sql, id, missing) => {
|
|
349
|
+
if (c.run(sql, [id]).changes === 0)
|
|
350
|
+
throw new OrbitError(missing);
|
|
351
|
+
};
|
|
352
|
+
const setBoard = (board) => {
|
|
353
|
+
onBoard(board.id);
|
|
354
|
+
if (checks.slugTaken(board.slug))
|
|
355
|
+
throw new OrbitError("board_slug_taken");
|
|
356
|
+
c.run(SQL.setBoard, valuesOf(BOARD.columns, board));
|
|
357
|
+
log.board = copyOf(BOARD, board, boardId);
|
|
358
|
+
};
|
|
359
|
+
const update = (of, id, patch, allowed, missing) => {
|
|
360
|
+
const changes = patched(patch, allowed);
|
|
361
|
+
const row = changes.length === 0
|
|
362
|
+
? c.get(`${of.select} WHERE id = ?`, [id])
|
|
363
|
+
: c.get(`UPDATE ${of.table} SET ${changes.map(([key]) => `${key} = ?`).join(", ")} WHERE id = ? RETURNING ${of.columns.join(", ")}`, [...changes.map(([, value]) => value), id]);
|
|
364
|
+
if (!row)
|
|
365
|
+
throw new OrbitError(missing);
|
|
366
|
+
return toRecord(of, row, boardId);
|
|
367
|
+
};
|
|
368
|
+
return {
|
|
369
|
+
...read,
|
|
370
|
+
async clear() {
|
|
371
|
+
await before();
|
|
372
|
+
c.exec(SQL.clear);
|
|
373
|
+
},
|
|
374
|
+
board: {
|
|
375
|
+
...read.board,
|
|
376
|
+
async set(board) {
|
|
377
|
+
await before();
|
|
378
|
+
setBoard(board);
|
|
379
|
+
},
|
|
380
|
+
async update(patch) {
|
|
381
|
+
await before();
|
|
382
|
+
const current = c.get(SQL.board);
|
|
383
|
+
if (!current)
|
|
384
|
+
throw new OrbitError("board_not_found");
|
|
385
|
+
const next = toRecord(BOARD, current, boardId);
|
|
386
|
+
Object.assign(next, Object.fromEntries(patched(patch, BOARD_PATCH)));
|
|
387
|
+
setBoard(next);
|
|
388
|
+
return copyOf(BOARD, next, boardId);
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
states: {
|
|
392
|
+
...read.states,
|
|
393
|
+
async insert(state) {
|
|
394
|
+
await before();
|
|
395
|
+
onBoard(state.board_id);
|
|
396
|
+
insert(STATE, state);
|
|
397
|
+
},
|
|
398
|
+
async update(id, patch) {
|
|
399
|
+
await before();
|
|
400
|
+
return update(STATE, id, patch, STATE_PATCH, "state_not_found");
|
|
401
|
+
},
|
|
402
|
+
async remove(id) {
|
|
403
|
+
await before();
|
|
404
|
+
if (c.run("DELETE FROM states WHERE id = ?", [id], { foreignKey: "state_has_tickets" }).changes === 0)
|
|
405
|
+
throw new OrbitError("state_not_found");
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
labels: {
|
|
409
|
+
...read.labels,
|
|
410
|
+
async insert(label) {
|
|
411
|
+
await before();
|
|
412
|
+
onBoard(label.board_id);
|
|
413
|
+
insert(LABEL, label);
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
tickets: {
|
|
417
|
+
...read.tickets,
|
|
418
|
+
async create(ticket) {
|
|
419
|
+
await before();
|
|
420
|
+
onBoard(ticket.board_id);
|
|
421
|
+
const row = c.get(SQL.createTicket, valuesOf(NEW_TICKET, ticket));
|
|
422
|
+
return copyOf(TICKET, { ...ticket, number: Number(row?.number) }, boardId);
|
|
423
|
+
},
|
|
424
|
+
async insert(ticket) {
|
|
425
|
+
await before();
|
|
426
|
+
onBoard(ticket.board_id);
|
|
427
|
+
insert(TICKET, ticket);
|
|
428
|
+
},
|
|
429
|
+
async update(id, patch) {
|
|
430
|
+
await before();
|
|
431
|
+
return update(TICKET, id, patch, TICKET_PATCH, "ticket_not_found");
|
|
432
|
+
},
|
|
433
|
+
async remove(id) {
|
|
434
|
+
await before();
|
|
435
|
+
removed(SQL.removeTicket, id, "ticket_not_found");
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
ticketLabels: {
|
|
439
|
+
...read.ticketLabels,
|
|
440
|
+
async set(ticketId, labelIds) {
|
|
441
|
+
await before();
|
|
442
|
+
if (labelIds.length === 0 && !c.get(SQL.ticketExists, [ticketId]))
|
|
443
|
+
throw new OrbitError("invalid_reference");
|
|
444
|
+
c.run(SQL.unlinkLabels, [ticketId]);
|
|
445
|
+
for (const labelId of new Set(labelIds))
|
|
446
|
+
c.run(SQL.linkLabel, [ticketId, labelId]);
|
|
447
|
+
},
|
|
448
|
+
async add(ticketId, labelId) {
|
|
449
|
+
await before();
|
|
450
|
+
c.run(SQL.linkLabel, [ticketId, labelId]);
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
comments: {
|
|
454
|
+
...read.comments,
|
|
455
|
+
async insert(comment) {
|
|
456
|
+
await before();
|
|
457
|
+
insert(COMMENT, comment);
|
|
458
|
+
},
|
|
459
|
+
async update(id, patch) {
|
|
460
|
+
await before();
|
|
461
|
+
const row = c.get(SQL.hideComment, [patch.omitted_for_ai, id]);
|
|
462
|
+
if (!row)
|
|
463
|
+
throw new OrbitError("comment_not_found");
|
|
464
|
+
return toRecord(COMMENT, row, boardId);
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
relations: {
|
|
468
|
+
...read.relations,
|
|
469
|
+
async insert(relation) {
|
|
470
|
+
await before();
|
|
471
|
+
insert(RELATION, relation);
|
|
472
|
+
},
|
|
473
|
+
async remove(id) {
|
|
474
|
+
await before();
|
|
475
|
+
removed(SQL.removeRelation, id, "relation_not_found");
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
crossBoardRelations: {
|
|
479
|
+
...read.crossBoardRelations,
|
|
480
|
+
async insert(relation) {
|
|
481
|
+
await before();
|
|
482
|
+
insert(CROSS, relation);
|
|
483
|
+
},
|
|
484
|
+
async remove(id) {
|
|
485
|
+
await before();
|
|
486
|
+
removed(SQL.removeCrossRelation, id, "relation_not_found");
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
entries: {
|
|
490
|
+
...read.entries,
|
|
491
|
+
async insert(entry) {
|
|
492
|
+
await before();
|
|
493
|
+
onBoard(entry.board_id);
|
|
494
|
+
insert(ENTRY, entry);
|
|
495
|
+
},
|
|
496
|
+
async update(id, patch) {
|
|
497
|
+
await before();
|
|
498
|
+
return update(ENTRY, id, patch, ENTRY_PATCH, "board_entry_not_found");
|
|
499
|
+
},
|
|
500
|
+
async remove(id) {
|
|
501
|
+
await before();
|
|
502
|
+
removed(SQL.removeEntry, id, "board_entry_not_found");
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
verdicts: {
|
|
506
|
+
...read.verdicts,
|
|
507
|
+
async insert(verdict) {
|
|
508
|
+
await before();
|
|
509
|
+
insert(VERDICT, verdict);
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
attachments: {
|
|
513
|
+
...read.attachments,
|
|
514
|
+
async insert(attachment) {
|
|
515
|
+
await before();
|
|
516
|
+
onBoard(attachment.board_id);
|
|
517
|
+
insert(ATTACHMENT, attachment);
|
|
518
|
+
},
|
|
519
|
+
async remove(id) {
|
|
520
|
+
await before();
|
|
521
|
+
removed(SQL.removeAttachment, id, "attachment_not_found");
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
events: {
|
|
525
|
+
...read.events,
|
|
526
|
+
async append(event) {
|
|
527
|
+
await before();
|
|
528
|
+
const { lastInsertRowid } = c.run(EVENT.insert, valuesOf(EVENT.columns, event));
|
|
529
|
+
log.events.push({ seq: lastInsertRowid, event: copyOf(EVENT, event, boardId) });
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
search: {
|
|
533
|
+
...read.search,
|
|
534
|
+
async index(ticketId, document) {
|
|
535
|
+
await before();
|
|
536
|
+
const { changes } = c.run(SQL.index, [document.title, document.description, document.type, document.details, document.comments, ticketId]);
|
|
537
|
+
if (changes === 0)
|
|
538
|
+
throw new OrbitError("invalid_reference");
|
|
539
|
+
},
|
|
540
|
+
async remove(ticketId) {
|
|
541
|
+
await before();
|
|
542
|
+
c.run(SQL.unindex, [ticketId]);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SQLite adapter's schema: every table Orbit 2.0 keeps on a computer or
|
|
3
|
+
* server, top to bottom. A data folder holds two kinds of database:
|
|
4
|
+
*
|
|
5
|
+
* registry.db the list of boards and this Orbit's settings
|
|
6
|
+
* boards/<folder>/board.db one board's records, one file per board
|
|
7
|
+
*
|
|
8
|
+
* with each board's images and markdown beside its board.db (the blob store,
|
|
9
|
+
* ./blobs.ts) and copies under backups/ (./backups.ts).
|
|
10
|
+
*
|
|
11
|
+
* Every file carries two numbers in its header: `application_id`, which says
|
|
12
|
+
* the file is Orbit's, and `user_version`, the version of the schema below.
|
|
13
|
+
* A file without them was written by Orbit 1.x or the first 2.0 (same tables,
|
|
14
|
+
* no version), and the adapter refuses to open it rather than change it: the
|
|
15
|
+
* migration brings such folders over.
|
|
16
|
+
*
|
|
17
|
+
* Conventions:
|
|
18
|
+
*
|
|
19
|
+
* - Field names are the records' own (src/core/model.ts), so a row and its
|
|
20
|
+
* record read the same.
|
|
21
|
+
* - A board's database holds one board, so its rows carry no `board_id`: the
|
|
22
|
+
* adapter adds the board's id when it hands a record out, and refuses a
|
|
23
|
+
* record naming another board.
|
|
24
|
+
* - `seq INTEGER PRIMARY KEY` keeps the order records were written, which the
|
|
25
|
+
* port's lists fall back on ("then by when it was written"). It is an
|
|
26
|
+
* explicit column because VACUUM (and so every backup) may renumber an
|
|
27
|
+
* implicit rowid. The record's own id is `UNIQUE` beside it.
|
|
28
|
+
* - Foreign keys hold the board together (`PRAGMA foreign_keys` is on for
|
|
29
|
+
* every connection): a comment's card exists, deleting a card deletes what
|
|
30
|
+
* belongs to it, a lane with cards can't be deleted. Two names are left
|
|
31
|
+
* unchecked on purpose, as the port says: a card's epic and a cross-board
|
|
32
|
+
* relation's target.
|
|
33
|
+
* - Flags are 0 or 1, times are ISO 8601 text in UTC, JSON stays text.
|
|
34
|
+
*/
|
|
35
|
+
/** Says "this file is Orbit's": 'ORBT' as a big-endian 32-bit integer. */
|
|
36
|
+
export declare const APPLICATION_ID = 1330790996;
|
|
37
|
+
/** The version of the schema below, in every file's `user_version`. */
|
|
38
|
+
export declare const SCHEMA_VERSION = 1;
|
|
39
|
+
/**
|
|
40
|
+
* The version of the search index's shape (its columns and tokenizer). When
|
|
41
|
+
* it changes, a board's index is rebuilt from the documents it holds, once,
|
|
42
|
+
* the first time the board is opened; never at every start.
|
|
43
|
+
*/
|
|
44
|
+
export declare const SEARCH_INDEX_VERSION = 1;
|
|
45
|
+
/**
|
|
46
|
+
* The search index's table, apart from the rest so a change of its shape
|
|
47
|
+
* rebuilds it alone (./board.ts). Its columns are the core's document
|
|
48
|
+
* (SearchDocument in src/store/types.ts).
|
|
49
|
+
*/
|
|
50
|
+
export declare const SEARCH_TABLE = "CREATE VIRTUAL TABLE search USING fts5 (\n title, description, type, details, comments,\n tokenize = \"unicode61 remove_diacritics 2 tokenchars '_'\"\n);";
|
|
51
|
+
/** registry.db */
|
|
52
|
+
export declare const REGISTRY_SCHEMA = "\n-- The boards this Orbit has. A board's records live in its own board.db, in\n-- boards/<folder>/; the folder is named once, by the slug the board was made\n-- with, and stays put when the slug changes. The name, slug and the rest are\n-- copies of the board's own record, kept in step after each write, so the\n-- list of boards is one read.\nCREATE TABLE boards (\n seq INTEGER PRIMARY KEY,\n id TEXT NOT NULL UNIQUE,\n slug TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n folder TEXT NOT NULL UNIQUE,\n repo_url TEXT NOT NULL,\n default_branch TEXT NOT NULL,\n last_active_at TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\nCREATE INDEX boards_by_name ON boards (name, seq);\n\n-- Small values this Orbit keeps for itself (the computer's default board).\nCREATE TABLE settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n";
|
|
53
|
+
/** board.db */
|
|
54
|
+
export declare const BOARD_SCHEMA: string;
|