@jant/core 0.7.0 → 0.7.1
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/bin/commands/export.js +3 -1
- package/bin/commands/import-site.js +689 -216
- package/bin/commands/setup.js +136 -0
- package/bin/commands/site/export.js +71 -34
- package/bin/commands/site/pull-media.js +2 -6
- package/bin/commands/site/snapshot/export.js +18 -60
- package/bin/commands/site/snapshot/import.js +22 -23
- package/bin/lib/d1-query.js +87 -2
- package/bin/lib/hugo-markdown.js +4 -0
- package/bin/lib/site-pull-media.js +21 -28
- package/bin/lib/site-selection.js +10 -1
- package/bin/lib/site-snapshot.js +338 -3
- package/bin/lib/sql-export.js +68 -5
- package/bin/lib/wrangler-cli.js +9 -0
- package/bin/lib/zip-archive.js +187 -0
- package/dist/{app-K_Aa1MMn.js → app-ZcaI1kPN.js} +732 -217
- package/dist/client/.vite/manifest.json +20 -20
- package/dist/client/_assets/chunks/{create-editor-CD3FhrOB.js → create-editor-B-m7X7S5.js} +50 -46
- package/dist/client/_assets/chunks/{sortable-list-CgaL2jCs.js → sortable-list-BJyd-LXE.js} +1 -1
- package/dist/client/_assets/chunks/{unsafe-svg-0QCkP0vZ.js → unsafe-svg-BkscJx69.js} +2 -2
- package/dist/client/_assets/{client-GiYENVw8.css → client-DAhoqPdr.css} +1 -1
- package/dist/client/_assets/{client-auth-k8gJ6QJj.js → client-auth-CAMfTrKW.js} +1 -1
- package/dist/client/_assets/{client-compose-B_kDtWkW.js → client-compose-D6rExKVK.js} +1 -1
- package/dist/client/_assets/{client-VnFxJN7G.js → client-knSEyUJO.js} +1 -1
- package/dist/client/_assets/{client-manage-M90aSTOk.js → client-manage-sVOkqVbH.js} +1 -1
- package/dist/client/_assets/{client-settings-DfYs9n1F.js → client-settings-CGR_nZ78.js} +7 -7
- package/dist/{github-sync-BPAvT999.js → github-sync-Gw4orAHk.js} +799 -110
- package/dist/index.js +2 -2
- package/dist/node.js +9 -5
- package/package.json +13 -3
- package/src/__tests__/dev-scripts.test.ts +203 -0
- package/src/__tests__/export-collection-order.test.ts +276 -0
- package/src/__tests__/export-feed-ids.test.ts +184 -0
- package/src/__tests__/export-feed-order.test.ts +294 -0
- package/src/__tests__/export-hugo-build.test.ts +130 -0
- package/src/__tests__/export-import-roundtrip.test.ts +94 -0
- package/src/__tests__/export-service.test.ts +611 -28
- package/src/__tests__/export-smart-collection.test.ts +329 -0
- package/src/__tests__/helpers/hugo-site.ts +146 -0
- package/src/__tests__/import-site-command.test.ts +487 -1
- package/src/__tests__/mise-config.test.ts +75 -4
- package/src/__tests__/node-dev-tasks.test.ts +264 -0
- package/src/__tests__/site-export-canonical-import.test.ts +149 -0
- package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
- package/src/__tests__/snapshot-settings.test.ts +97 -0
- package/src/__tests__/snapshot-tables.test.ts +219 -0
- package/src/__tests__/sql-export.test.ts +173 -0
- package/src/__tests__/zip-archive.test.ts +106 -0
- package/src/app.tsx +15 -6
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +1 -1
- package/src/client/components/__tests__/jant-settings-general.test.ts +21 -1
- package/src/client/components/jant-repo-picker-types.ts +6 -1
- package/src/client/components/jant-repo-picker.ts +4 -7
- package/src/client/components/jant-settings-general.ts +17 -12
- package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
- package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
- package/src/client/tiptap/extensions.ts +0 -3
- package/src/client/tiptap/structural-keymap.ts +158 -47
- package/src/db/__tests__/d1-query.test.ts +56 -1
- package/src/i18n/locales/settings/en.po +8 -8
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +8 -8
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +8 -8
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
- package/src/lib/__tests__/image.test.ts +27 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
- package/src/lib/__tests__/markdown.test.ts +10 -0
- package/src/lib/__tests__/resolve-config.test.ts +67 -0
- package/src/lib/__tests__/schemas.test.ts +27 -1
- package/src/lib/__tests__/timeline.test.ts +87 -0
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
- package/src/lib/discover.ts +3 -1
- package/src/lib/github-sync-repo-name.ts +45 -0
- package/src/lib/hugo-markdown.ts +46 -0
- package/src/lib/image.ts +17 -4
- package/src/lib/markdown-manager.ts +392 -2
- package/src/lib/post-body-html.ts +11 -4
- package/src/lib/resolve-config.ts +58 -1
- package/src/lib/schemas.ts +50 -5
- package/src/lib/thread-fold.ts +3 -3
- package/src/lib/timeline.ts +1 -1
- package/src/lib/tiptap-to-markdown.ts +13 -7
- package/src/lib/url.ts +20 -0
- package/src/lib/view.ts +2 -2
- package/src/node/__tests__/cli-setup.test.ts +163 -0
- package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
- package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
- package/src/node/__tests__/runtime.test.ts +38 -0
- package/src/node/index.ts +2 -0
- package/src/node/request-handler.ts +3 -1
- package/src/routes/api/__tests__/posts.test.ts +24 -0
- package/src/routes/api/__tests__/upload.test.ts +34 -0
- package/src/routes/api/export.ts +3 -3
- package/src/routes/api/internal/sites.ts +0 -1
- package/src/routes/api/posts.ts +2 -0
- package/src/routes/api/public/posts.ts +21 -1
- package/src/routes/api/upload.ts +10 -3
- package/src/routes/compose.tsx +4 -0
- package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
- package/src/routes/dash/settings.tsx +212 -70
- package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
- package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
- package/src/routes/pages/archive.tsx +17 -11
- package/src/routes/pages/featured.tsx +11 -5
- package/src/routes/pages/page.tsx +53 -31
- package/src/routes/pages/search.tsx +4 -2
- package/src/runtime/__tests__/readiness.test.ts +23 -0
- package/src/runtime/node.ts +49 -0
- package/src/runtime/readiness.ts +15 -0
- package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
- package/src/services/__tests__/custom-url.test.ts +34 -0
- package/src/services/__tests__/github-app-installations.test.ts +153 -0
- package/src/services/__tests__/github-sync-push.test.ts +46 -0
- package/src/services/__tests__/media.test.ts +31 -0
- package/src/services/__tests__/path.test.ts +56 -0
- package/src/services/__tests__/post-timeline.test.ts +127 -0
- package/src/services/__tests__/post.test.ts +74 -0
- package/src/services/bootstrap.ts +263 -44
- package/src/services/custom-url.ts +2 -2
- package/src/services/export-theme/layouts/_default/alias.html +27 -1
- package/src/services/export-theme/layouts/_default/list.html +2 -63
- package/src/services/export-theme/layouts/_default/rss.xml +27 -38
- package/src/services/export-theme/layouts/collections/list.html +3 -3
- package/src/services/export-theme/layouts/featured/list.html +1 -4
- package/src/services/export-theme/layouts/index.html +40 -26
- package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
- package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
- package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
- package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/head.html +1 -1
- package/src/services/export-theme/layouts/partials/header.html +5 -3
- package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
- package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
- package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
- package/src/services/export-theme/layouts/partials/thread-preview.html +1 -1
- package/src/services/export-theme/layouts/post/list.html +1 -1
- package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
- package/src/services/export-theme/styles/main.css +0 -1
- package/src/services/export-theme/theme.toml +1 -1
- package/src/services/export.ts +620 -71
- package/src/services/github-app-installations.ts +171 -4
- package/src/services/github-sync.ts +69 -15
- package/src/services/mcp.ts +19 -1
- package/src/services/media.ts +8 -1
- package/src/services/path.ts +34 -1
- package/src/services/post.ts +214 -39
- package/src/services/site-admin.ts +3 -7
- package/src/services/site.ts +83 -31
- package/src/styles/ui.css +7 -1
- package/src/types/app-context.ts +16 -0
- package/src/types/bindings.ts +5 -0
- package/src/types/operations.ts +10 -0
- package/src/ui/dash/settings/GeneralContent.tsx +10 -8
- package/src/client/tiptap/exitable-marks.ts +0 -73
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
} from "node:fs/promises";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import { basename, dirname, extname, join, relative } from "node:path";
|
|
15
|
-
import { unzipSync, zipSync } from "fflate";
|
|
16
15
|
import { parse, stringify } from "smol-toml";
|
|
17
16
|
import {
|
|
18
17
|
collectMediaReferences as collectParsedMediaReferences,
|
|
@@ -20,6 +19,7 @@ import {
|
|
|
20
19
|
rewriteMediaReferences,
|
|
21
20
|
} from "./site-media-parser.js";
|
|
22
21
|
import { formatFrontMatter, parseFrontMatter } from "./hugo-markdown.js";
|
|
22
|
+
import { extractZipFile, writeDirectoryToZip } from "./zip-archive.js";
|
|
23
23
|
|
|
24
24
|
export function getSitePathPrefix(baseUrl) {
|
|
25
25
|
if (typeof baseUrl !== "string" || baseUrl.trim() === "") {
|
|
@@ -477,29 +477,6 @@ async function resolveExistingPulledPath(
|
|
|
477
477
|
return toPulledPublicPath(pathname, sitePathPrefix);
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
-
async function packDirectoryToZip(rootDir) {
|
|
481
|
-
const files = {};
|
|
482
|
-
const allFiles = await walkFiles(rootDir);
|
|
483
|
-
|
|
484
|
-
for (const fullPath of allFiles) {
|
|
485
|
-
const relPath = relative(rootDir, fullPath).replace(/\\/g, "/");
|
|
486
|
-
files[relPath] = new Uint8Array(await readFile(fullPath));
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
return zipSync(files);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
async function unpackZipToDirectory(zipBytes, rootDir) {
|
|
493
|
-
const files = unzipSync(zipBytes);
|
|
494
|
-
await Promise.all(
|
|
495
|
-
Object.entries(files).map(async ([relPath, bytes]) => {
|
|
496
|
-
const fullPath = join(rootDir, relPath);
|
|
497
|
-
await mkdir(dirname(fullPath), { recursive: true });
|
|
498
|
-
await writeFile(fullPath, bytes);
|
|
499
|
-
}),
|
|
500
|
-
);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
480
|
export async function pullSiteExportDirectory(rootDir, options = {}) {
|
|
504
481
|
const logger =
|
|
505
482
|
typeof options.logger === "function" ? options.logger : () => {};
|
|
@@ -681,14 +658,30 @@ export async function pullSiteExportDirectory(rootDir, options = {}) {
|
|
|
681
658
|
return stats;
|
|
682
659
|
}
|
|
683
660
|
|
|
684
|
-
|
|
661
|
+
/**
|
|
662
|
+
* Pull media into an export ZIP: extract it to a temporary directory, pull
|
|
663
|
+
* there, and write the result as a new archive. Nothing is held in memory
|
|
664
|
+
* beyond one media file at a time, so the archive can be any size.
|
|
665
|
+
*
|
|
666
|
+
* @param {string} inputZipPath - Export archive to read
|
|
667
|
+
* @param {string} outputZipPath - Archive to write; may be the input
|
|
668
|
+
* @param {Parameters<typeof pullSiteExportDirectory>[1]} [options]
|
|
669
|
+
* @returns {Promise<Awaited<ReturnType<typeof pullSiteExportDirectory>>>} Pull stats
|
|
670
|
+
* @example
|
|
671
|
+
* await pullSiteExportZipFile("export.zip", "export.zip", { logger });
|
|
672
|
+
*/
|
|
673
|
+
export async function pullSiteExportZipFile(
|
|
674
|
+
inputZipPath,
|
|
675
|
+
outputZipPath,
|
|
676
|
+
options = {},
|
|
677
|
+
) {
|
|
685
678
|
const tempDir = await mkdtemp(join(tmpdir(), "jant-site-pull-"));
|
|
686
679
|
|
|
687
680
|
try {
|
|
688
|
-
await
|
|
681
|
+
await extractZipFile(inputZipPath, tempDir);
|
|
689
682
|
const stats = await pullSiteExportDirectory(tempDir, options);
|
|
690
|
-
|
|
691
|
-
return
|
|
683
|
+
await writeDirectoryToZip(tempDir, outputZipPath);
|
|
684
|
+
return stats;
|
|
692
685
|
} finally {
|
|
693
686
|
await rm(tempDir, { recursive: true, force: true });
|
|
694
687
|
}
|
|
@@ -176,6 +176,15 @@ function formatSiteSummary(rows) {
|
|
|
176
176
|
.join(", ");
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
/**
|
|
180
|
+
* How the target instance maps requests to sites.
|
|
181
|
+
*
|
|
182
|
+
* @param {{ SITE_RESOLUTION_MODE?: unknown }} [env] The process environment,
|
|
183
|
+
* or runtime bindings built from it; defaults to `process.env`
|
|
184
|
+
* @returns {"host-based" | "single-site"} `host-based` only when set to it
|
|
185
|
+
* @example
|
|
186
|
+
* if (getCliSiteResolutionMode(process.env) === "host-based") { ... }
|
|
187
|
+
*/
|
|
179
188
|
export function getCliSiteResolutionMode(env = process.env) {
|
|
180
189
|
return env.SITE_RESOLUTION_MODE === "host-based"
|
|
181
190
|
? "host-based"
|
|
@@ -192,7 +201,7 @@ export function getCliSiteResolutionMode(env = process.env) {
|
|
|
192
201
|
*
|
|
193
202
|
* @param {{ query(sql: string): Promise<Record<string, unknown>[]>, execute?(sql: string): Promise<void> }} queryRunner
|
|
194
203
|
* Runs SQL against the target database
|
|
195
|
-
* @param {{ env?:
|
|
204
|
+
* @param {{ env?: { SITE_RESOLUTION_MODE?: unknown }, site?: string, host?: string, pathPrefix?: string, url?: string, createIfMissing?: boolean, bootstrapSite?: { id?: string, key?: string } }} [options]
|
|
196
205
|
* The environment, the command's site flags, and whether single-site mode
|
|
197
206
|
* may create the site shell when none exists
|
|
198
207
|
* @returns {Promise<{ created: boolean, site: { id: string, key: string, status: string, createdAt: number, updatedAt: number } }>}
|
package/bin/lib/site-snapshot.js
CHANGED
|
@@ -5,31 +5,101 @@ export const SNAPSHOT_FORMAT = "jant-site-snapshot";
|
|
|
5
5
|
export const SNAPSHOT_VERSION = 2;
|
|
6
6
|
export const SUPPORTED_SNAPSHOT_VERSIONS = [1, SNAPSHOT_VERSION];
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Site content, in the order rows are inserted on import.
|
|
10
|
+
*
|
|
11
|
+
* Membership is what this list decides: a table absent from it is never read,
|
|
12
|
+
* so its rows leave no trace in the snapshot and `--replace` cannot clear them.
|
|
13
|
+
*
|
|
14
|
+
* The order here is kept as a foreign-key topological sort for readability, but
|
|
15
|
+
* it is not what an export follows — `sortExportTables` re-sorts by
|
|
16
|
+
* `TABLE_EXPORT_ORDER` in `sql-export.js`, and that is the list to change when
|
|
17
|
+
* the dump order has to change. `src/__tests__/snapshot-tables.test.ts` checks
|
|
18
|
+
* the sorted result for that reason.
|
|
19
|
+
*/
|
|
8
20
|
export const SNAPSHOT_TABLES = [
|
|
9
21
|
"site_setting",
|
|
10
22
|
"collection",
|
|
11
|
-
"
|
|
12
|
-
"collection_directory_item",
|
|
23
|
+
"smart_collection",
|
|
13
24
|
"post",
|
|
14
25
|
"thread_collection",
|
|
26
|
+
"nav_item",
|
|
27
|
+
"collection_directory_item",
|
|
15
28
|
"path_registry",
|
|
16
29
|
"media",
|
|
17
30
|
];
|
|
18
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The same content, in the order `--replace` deletes it: children first, so no
|
|
34
|
+
* delete depends on a cascade to clean up after it. `site_setting` is absent on
|
|
35
|
+
* purpose — `buildReplaceSql` clears it by key, because a site's settings hold
|
|
36
|
+
* more than the snapshot carries.
|
|
37
|
+
*/
|
|
19
38
|
export const SNAPSHOT_CLEAR_TABLES = [
|
|
20
|
-
"thread_collection",
|
|
21
39
|
"media",
|
|
22
40
|
"path_registry",
|
|
23
41
|
"collection_directory_item",
|
|
24
42
|
"nav_item",
|
|
43
|
+
"thread_collection",
|
|
25
44
|
"post",
|
|
45
|
+
"smart_collection",
|
|
26
46
|
"collection",
|
|
27
47
|
];
|
|
28
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Site-scoped tables the snapshot deliberately leaves alone.
|
|
51
|
+
*
|
|
52
|
+
* Every table carrying a `site_id` has to appear here or in `SNAPSHOT_TABLES`
|
|
53
|
+
* — `src/__tests__/snapshot-tables.test.ts` fails the build otherwise, so a new
|
|
54
|
+
* table forces a decision rather than being silently dropped. The reasons fall
|
|
55
|
+
* into three groups: auth and routing shell that a content restore must
|
|
56
|
+
* preserve, credentials and external integration bindings that do not travel
|
|
57
|
+
* with content, and transient runtime bookkeeping.
|
|
58
|
+
*/
|
|
59
|
+
export const SNAPSHOT_EXCLUDED_TABLES = [
|
|
60
|
+
// Auth and routing shell. `--replace` restores content into an existing
|
|
61
|
+
// site; its members and domains belong to the deployment, not the content.
|
|
62
|
+
"site_member",
|
|
63
|
+
"site_domain",
|
|
64
|
+
// Credentials. Reissued per deployment, never copied between sites. The demo
|
|
65
|
+
// rebuild clears these through the internal admin route instead.
|
|
66
|
+
"api_token",
|
|
67
|
+
// External integration bindings. They point at an installation or a chat on
|
|
68
|
+
// someone else's service, so restoring them into another site would aim that
|
|
69
|
+
// site at a binding it does not own.
|
|
70
|
+
"github_app_installation",
|
|
71
|
+
"telegram_binding",
|
|
72
|
+
"telegram_pending_binding",
|
|
73
|
+
"telegram_media_group_item",
|
|
74
|
+
// Transient runtime bookkeeping, rebuilt by the runtime as it goes:
|
|
75
|
+
// in-flight uploads, and the recycle-window ledger for deleted objects.
|
|
76
|
+
"upload_session",
|
|
77
|
+
"storage_purge",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Settings the snapshot carries: the site's published identity and appearance.
|
|
82
|
+
*
|
|
83
|
+
* `site_setting` holds more than a site's content — deployment wiring, secrets,
|
|
84
|
+
* integration bindings, local UI state — so both the export and `--replace`
|
|
85
|
+
* work through this allowlist rather than the whole table. Membership is what
|
|
86
|
+
* it decides: a key absent from it is neither exported nor cleared on import.
|
|
87
|
+
*
|
|
88
|
+
* Every DB-backed key in `CONFIG_FIELDS` has to appear here or in
|
|
89
|
+
* `SNAPSHOT_EXCLUDED_SETTING_KEYS`, and
|
|
90
|
+
* `src/__tests__/snapshot-settings.test.ts` fails the build otherwise, so a new
|
|
91
|
+
* setting forces a decision instead of being silently dropped.
|
|
92
|
+
*/
|
|
29
93
|
export const SNAPSHOT_SETTING_KEYS = [
|
|
30
94
|
"SITE_NAME",
|
|
31
95
|
"SITE_DESCRIPTION",
|
|
96
|
+
// The language trio travels together. `post.language` and
|
|
97
|
+
// `post.translation_group_id` are already carried by the whole-row `post`
|
|
98
|
+
// dump, so leaving the switch behind restored a site whose posts were
|
|
99
|
+
// stamped per language while every per-language view was gone.
|
|
32
100
|
"SITE_LANGUAGE",
|
|
101
|
+
"MULTILINGUAL_ENABLED",
|
|
102
|
+
"ADDITIONAL_LANGUAGES",
|
|
33
103
|
"MAIN_RSS_FEED",
|
|
34
104
|
"PUBLIC_API_ENABLED",
|
|
35
105
|
"RSS_FEEDS_ENABLED",
|
|
@@ -48,6 +118,68 @@ export const SNAPSHOT_SETTING_KEYS = [
|
|
|
48
118
|
"NOINDEX",
|
|
49
119
|
];
|
|
50
120
|
|
|
121
|
+
/**
|
|
122
|
+
* DB-backed settings the snapshot deliberately leaves alone.
|
|
123
|
+
*
|
|
124
|
+
* `envOnly` keys are absent from both lists on purpose: they never reach
|
|
125
|
+
* `site_setting`, so there is no row to carry or clear.
|
|
126
|
+
*/
|
|
127
|
+
export const SNAPSHOT_EXCLUDED_SETTING_KEYS = [
|
|
128
|
+
// The operator's own dashboard locale, not the site's published language.
|
|
129
|
+
// A content restore should not relabel the dashboard of whoever runs the
|
|
130
|
+
// target site.
|
|
131
|
+
"DASHBOARD_LANGUAGE",
|
|
132
|
+
// Sizing and pacing knobs. They describe how much the target deployment
|
|
133
|
+
// serves at a time, not what it publishes, and they carry env defaults the
|
|
134
|
+
// deployment picked.
|
|
135
|
+
"ARCHIVE_DEFAULT_LAYOUT",
|
|
136
|
+
"PAGE_SIZE",
|
|
137
|
+
"SEARCH_PAGE_SIZE",
|
|
138
|
+
"ARCHIVE_PAGE_SIZE",
|
|
139
|
+
"SUMMARY_MAX_PARAGRAPHS",
|
|
140
|
+
"SUMMARY_MAX_CHARS",
|
|
141
|
+
"RSS_FEED_LIMIT",
|
|
142
|
+
"RSS_PUBLISH_DELAY_SECONDS",
|
|
143
|
+
// Code injection. `CUSTOM_CSS` is declarative and travels; these two are
|
|
144
|
+
// executable markup, and importing an archive must not be a way to run
|
|
145
|
+
// script on the importing site.
|
|
146
|
+
"CUSTOM_HEAD_HTML",
|
|
147
|
+
"CUSTOM_BODY_END_HTML",
|
|
148
|
+
// Being listed in the Jant Discover directory is the site owner's consent,
|
|
149
|
+
// and the announcement state below it belongs to the announcing instance.
|
|
150
|
+
"DISCOVER",
|
|
151
|
+
"DISCOVER_ANNOUNCE_STATE",
|
|
152
|
+
// Local UI state: which affordances this author has already been shown, and
|
|
153
|
+
// how far setup got.
|
|
154
|
+
"DISCOVERY_COMPOSE_OPEN_SHORTCUT_AT",
|
|
155
|
+
"DISCOVERY_SLASH_COMMAND_AT",
|
|
156
|
+
"ONBOARDING_STATUS",
|
|
157
|
+
// Credential. Reissued per deployment, never copied between sites.
|
|
158
|
+
"PASSWORD_RESET_TOKEN",
|
|
159
|
+
// External integration bindings, their secrets, and their sync bookkeeping.
|
|
160
|
+
// They point at a repo, an installation, or a chat on someone else's
|
|
161
|
+
// service, so restoring them would aim the target site at a binding it does
|
|
162
|
+
// not own — the same reason `github_app_installation` and `telegram_binding`
|
|
163
|
+
// are excluded tables.
|
|
164
|
+
"GITHUB_SYNC_ENABLED",
|
|
165
|
+
"GITHUB_SYNC_REPO",
|
|
166
|
+
"GITHUB_SYNC_TOKEN",
|
|
167
|
+
"GITHUB_SYNC_WEBHOOK_SECRET",
|
|
168
|
+
"GITHUB_SYNC_WEBHOOK_ID",
|
|
169
|
+
"GITHUB_SYNC_LAST_PUSH_SHA",
|
|
170
|
+
"GITHUB_SYNC_LAST_PUSH_AT",
|
|
171
|
+
"GITHUB_SYNC_PENDING",
|
|
172
|
+
"GITHUB_SYNC_PENDING_AT",
|
|
173
|
+
"GITHUB_SYNC_DIRTY",
|
|
174
|
+
"GITHUB_SYNC_LAST_ERROR",
|
|
175
|
+
"GITHUB_SYNC_AUTH_MODE",
|
|
176
|
+
"GITHUB_SYNC_APP_INSTALLATION_ID",
|
|
177
|
+
"TELEGRAM_BOT_TOKEN",
|
|
178
|
+
"TELEGRAM_BOT_ID",
|
|
179
|
+
"TELEGRAM_BOT_USERNAME",
|
|
180
|
+
"TELEGRAM_BOT_WEBHOOK_SECRET",
|
|
181
|
+
];
|
|
182
|
+
|
|
51
183
|
function escapeSqlString(value) {
|
|
52
184
|
return String(value).replaceAll("'", "''");
|
|
53
185
|
}
|
|
@@ -78,6 +210,12 @@ const SELECT_SQL_BY_TABLE = {
|
|
|
78
210
|
WHERE "site_id" = ?1
|
|
79
211
|
ORDER BY "position", "id"
|
|
80
212
|
`,
|
|
213
|
+
smart_collection: `
|
|
214
|
+
SELECT *
|
|
215
|
+
FROM "smart_collection"
|
|
216
|
+
WHERE "site_id" = ?1
|
|
217
|
+
ORDER BY "created_at", "id"
|
|
218
|
+
`,
|
|
81
219
|
post: `
|
|
82
220
|
SELECT *
|
|
83
221
|
FROM "post"
|
|
@@ -118,6 +256,14 @@ export function getSnapshotSelectSql(tableName, siteId) {
|
|
|
118
256
|
return statement.trim().replaceAll("?1", `'${escapeSqlString(siteId)}'`);
|
|
119
257
|
}
|
|
120
258
|
|
|
259
|
+
/**
|
|
260
|
+
* SQL listing a site's stored media objects: originals and video posters.
|
|
261
|
+
*
|
|
262
|
+
* @param {string} siteId Site whose media rows to read
|
|
263
|
+
* @returns {string} A query returning `key` and `contentType` columns
|
|
264
|
+
* @example
|
|
265
|
+
* collectSnapshotObjects(await query(buildSnapshotStorageQuery(site.id)))
|
|
266
|
+
*/
|
|
121
267
|
export function buildSnapshotStorageQuery(siteId) {
|
|
122
268
|
return `
|
|
123
269
|
SELECT "key", "contentType"
|
|
@@ -146,6 +292,27 @@ export function buildSnapshotStorageQuery(siteId) {
|
|
|
146
292
|
`.trim();
|
|
147
293
|
}
|
|
148
294
|
|
|
295
|
+
/**
|
|
296
|
+
* A storage object a snapshot carries.
|
|
297
|
+
*
|
|
298
|
+
* @typedef {object} SnapshotObject
|
|
299
|
+
* @property {string} key Storage key
|
|
300
|
+
* @property {string} contentType MIME type, or `""` when neither the row nor
|
|
301
|
+
* the key's extension gives one
|
|
302
|
+
*/
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Deduplicate the rows of `buildSnapshotStorageQuery` into storage objects.
|
|
306
|
+
*
|
|
307
|
+
* Rows with a blank key are skipped. A row without a content type takes one
|
|
308
|
+
* from the key's extension.
|
|
309
|
+
*
|
|
310
|
+
* @param {Record<string, unknown>[]} rows Rows with `key` and `contentType`
|
|
311
|
+
* @returns {SnapshotObject[]} One entry per distinct key
|
|
312
|
+
* @example
|
|
313
|
+
* collectSnapshotObjects([{ key: "media/a.png", contentType: null }])
|
|
314
|
+
* // => [{ key: "media/a.png", contentType: "image/png" }]
|
|
315
|
+
*/
|
|
149
316
|
export function collectSnapshotObjects(rows) {
|
|
150
317
|
const objects = new Map();
|
|
151
318
|
|
|
@@ -198,6 +365,28 @@ export function buildSnapshotMeta(site, options = {}) {
|
|
|
198
365
|
};
|
|
199
366
|
}
|
|
200
367
|
|
|
368
|
+
/**
|
|
369
|
+
* A snapshot's `meta.json`, as `assertSnapshotMeta` accepts it.
|
|
370
|
+
*
|
|
371
|
+
* @typedef {object} SnapshotMeta
|
|
372
|
+
* @property {typeof SNAPSHOT_FORMAT} format
|
|
373
|
+
* @property {number} version One of `SUPPORTED_SNAPSHOT_VERSIONS`
|
|
374
|
+
* @property {"sqlite" | "pg"} [dialect] Absent from snapshots that predate it
|
|
375
|
+
* @property {{ id: string, key: string }} [site] The site the snapshot was
|
|
376
|
+
* exported from; absent from legacy snapshots
|
|
377
|
+
* @property {unknown} [tables] Listed by legacy snapshots only
|
|
378
|
+
*/
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Refuse a `meta.json` this version cannot import.
|
|
382
|
+
*
|
|
383
|
+
* @param {unknown} meta Parsed `meta.json`
|
|
384
|
+
* @returns {asserts meta is SnapshotMeta}
|
|
385
|
+
* @throws {Error} When the format, version, dialect, or site is not supported
|
|
386
|
+
* @example
|
|
387
|
+
* const meta = JSON.parse(await readFile(metaPath, "utf8"));
|
|
388
|
+
* assertSnapshotMeta(meta);
|
|
389
|
+
*/
|
|
201
390
|
export function assertSnapshotMeta(meta) {
|
|
202
391
|
if (!meta || typeof meta !== "object") {
|
|
203
392
|
throw new Error("Snapshot meta.json is missing or invalid.");
|
|
@@ -274,6 +463,15 @@ export function isLegacySnapshotMeta(meta) {
|
|
|
274
463
|
return !meta?.site || tables.includes("setting");
|
|
275
464
|
}
|
|
276
465
|
|
|
466
|
+
/**
|
|
467
|
+
* The site a snapshot was exported from, for creating or remapping the target.
|
|
468
|
+
*
|
|
469
|
+
* @param {SnapshotMeta} meta Snapshot meta accepted by `assertSnapshotMeta`
|
|
470
|
+
* @returns {{ id: string, key: string } | undefined} The source site, or
|
|
471
|
+
* undefined for a legacy snapshot, which records none
|
|
472
|
+
* @example
|
|
473
|
+
* const snapshotSite = getSnapshotBootstrapSite(meta);
|
|
474
|
+
*/
|
|
277
475
|
export function getSnapshotBootstrapSite(meta) {
|
|
278
476
|
if (isLegacySnapshotMeta(meta)) {
|
|
279
477
|
return undefined;
|
|
@@ -285,6 +483,18 @@ export function getSnapshotBootstrapSite(meta) {
|
|
|
285
483
|
};
|
|
286
484
|
}
|
|
287
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Refuse to import a snapshot into a site other than the one it came from.
|
|
488
|
+
*
|
|
489
|
+
* Legacy snapshots record no site and pass.
|
|
490
|
+
*
|
|
491
|
+
* @param {SnapshotMeta} meta Snapshot meta accepted by `assertSnapshotMeta`
|
|
492
|
+
* @param {{ id: string }} site The import target
|
|
493
|
+
* @returns {void}
|
|
494
|
+
* @throws {Error} When the snapshot names a different site
|
|
495
|
+
* @example
|
|
496
|
+
* validateSnapshotTargetSite(meta, targetSite);
|
|
497
|
+
*/
|
|
288
498
|
export function validateSnapshotTargetSite(meta, site) {
|
|
289
499
|
if (isLegacySnapshotMeta(meta)) {
|
|
290
500
|
return;
|
|
@@ -297,6 +507,17 @@ export function validateSnapshotTargetSite(meta, site) {
|
|
|
297
507
|
}
|
|
298
508
|
}
|
|
299
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Replace a snapshot's source site id with the target's throughout its SQL.
|
|
512
|
+
*
|
|
513
|
+
* @param {string} sql Snapshot `db.sql`
|
|
514
|
+
* @param {string} sourceSiteId Site the snapshot was exported from; empty
|
|
515
|
+
* leaves the SQL unchanged
|
|
516
|
+
* @param {string} targetSiteId Site the snapshot is imported into
|
|
517
|
+
* @returns {string} The rewritten SQL
|
|
518
|
+
* @example
|
|
519
|
+
* rewriteSnapshotSiteIdentifiers(dbSql, snapshotSite.id, targetSite.id)
|
|
520
|
+
*/
|
|
300
521
|
export function rewriteSnapshotSiteIdentifiers(
|
|
301
522
|
sql,
|
|
302
523
|
sourceSiteId,
|
|
@@ -311,6 +532,17 @@ export function rewriteSnapshotSiteIdentifiers(
|
|
|
311
532
|
return sql.replaceAll(escapedSource, escapedTarget);
|
|
312
533
|
}
|
|
313
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Move a storage key from the source site's namespace to the target's.
|
|
537
|
+
*
|
|
538
|
+
* @param {string} key Storage key as exported
|
|
539
|
+
* @param {string} sourceSiteId Site the snapshot was exported from; empty
|
|
540
|
+
* leaves the key unchanged
|
|
541
|
+
* @param {string} targetSiteId Site the snapshot is imported into
|
|
542
|
+
* @returns {string} The key to store the object under
|
|
543
|
+
* @example
|
|
544
|
+
* remapSnapshotObjectKey(entry.key, snapshotSite.id, targetSite.id)
|
|
545
|
+
*/
|
|
314
546
|
export function remapSnapshotObjectKey(key, sourceSiteId, targetSiteId) {
|
|
315
547
|
if (!sourceSiteId || sourceSiteId === targetSiteId) {
|
|
316
548
|
return key;
|
|
@@ -325,6 +557,12 @@ export function remapSnapshotObjectKey(key, sourceSiteId, targetSiteId) {
|
|
|
325
557
|
* export time (with forward slashes). If the snapshot was produced by a
|
|
326
558
|
* different site than the import target, callers apply
|
|
327
559
|
* `remapSnapshotObjectKey()` before uploading.
|
|
560
|
+
*
|
|
561
|
+
* @param {string} rootDir Snapshot directory
|
|
562
|
+
* @returns {Promise<{ key: string, filePath: string, contentType: string }[]>}
|
|
563
|
+
* Files sorted by key; empty when there is no `objects/` directory
|
|
564
|
+
* @example
|
|
565
|
+
* const objectFiles = await enumerateSnapshotObjectFiles(snapshotDir);
|
|
328
566
|
*/
|
|
329
567
|
export async function enumerateSnapshotObjectFiles(rootDir) {
|
|
330
568
|
const objectsRoot = join(rootDir, "objects");
|
|
@@ -485,6 +723,16 @@ function splitSqlStatements(sql) {
|
|
|
485
723
|
return statements;
|
|
486
724
|
}
|
|
487
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Scope a legacy single-site dump to a site: global `setting` rows become
|
|
728
|
+
* `site_setting` rows, and site-owned tables gain a `site_id` column.
|
|
729
|
+
*
|
|
730
|
+
* @param {string} sql Legacy snapshot `db.sql`
|
|
731
|
+
* @param {string} siteId Site the snapshot is imported into
|
|
732
|
+
* @returns {string} Site-scoped SQL
|
|
733
|
+
* @example
|
|
734
|
+
* rewriteLegacySnapshotSql(dbSql, targetSite.id)
|
|
735
|
+
*/
|
|
488
736
|
export function rewriteLegacySnapshotSql(sql, siteId) {
|
|
489
737
|
const rewrittenStatements = splitSqlStatements(sql).map((statement) => {
|
|
490
738
|
const normalized = statement.trim();
|
|
@@ -840,6 +1088,58 @@ function parseSqlScalar(raw) {
|
|
|
840
1088
|
return trimmed;
|
|
841
1089
|
}
|
|
842
1090
|
|
|
1091
|
+
/**
|
|
1092
|
+
* SQL clearing a site's snapshot content before a replacing import.
|
|
1093
|
+
*
|
|
1094
|
+
* @param {string} siteId Site to clear
|
|
1095
|
+
* @returns {string} `DELETE` statements for every `SNAPSHOT_CLEAR_TABLES`
|
|
1096
|
+
* table and the snapshot's setting keys
|
|
1097
|
+
* @example
|
|
1098
|
+
* await execute(`${buildReplaceSql(site.id)}\n${dbSql}`);
|
|
1099
|
+
*/
|
|
1100
|
+
/**
|
|
1101
|
+
* Order `post` rows so every row comes after its Thread root and the post it
|
|
1102
|
+
* replies to.
|
|
1103
|
+
*
|
|
1104
|
+
* The dump reads posts by creation time, and `(site_id, thread_id)` and
|
|
1105
|
+
* `(site_id, reply_to_id)` are foreign keys. A post moved into a Thread keeps
|
|
1106
|
+
* its own creation time, and an import restores creation times, so a reply
|
|
1107
|
+
* can be older than its root; inserted in creation order it names a row that
|
|
1108
|
+
* isn't there yet, and the whole import fails. Otherwise the order is kept.
|
|
1109
|
+
*
|
|
1110
|
+
* @param {Record<string, unknown>[]} rows - `post` rows in dump order
|
|
1111
|
+
* @returns {Record<string, unknown>[]} The same rows, parents first
|
|
1112
|
+
* @example
|
|
1113
|
+
* orderSnapshotPostRows([reply, root]); // [root, reply]
|
|
1114
|
+
*/
|
|
1115
|
+
export function orderSnapshotPostRows(rows) {
|
|
1116
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
1117
|
+
const placed = new Set();
|
|
1118
|
+
const ordered = [];
|
|
1119
|
+
|
|
1120
|
+
const place = (row, visiting) => {
|
|
1121
|
+
if (placed.has(row.id) || visiting.has(row.id)) return;
|
|
1122
|
+
visiting.add(row.id);
|
|
1123
|
+
for (const parentId of [row.thread_id, row.reply_to_id]) {
|
|
1124
|
+
const parent = parentId !== row.id ? byId.get(parentId) : undefined;
|
|
1125
|
+
if (parent) place(parent, visiting);
|
|
1126
|
+
}
|
|
1127
|
+
placed.add(row.id);
|
|
1128
|
+
ordered.push(row);
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
for (const row of rows) place(row, new Set());
|
|
1132
|
+
return ordered;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* SQL a snapshot import runs before its inserts on SQLite and D1: foreign
|
|
1137
|
+
* keys are checked at commit rather than per row, so a snapshot written
|
|
1138
|
+
* before posts were ordered parents-first still loads. Postgres has no
|
|
1139
|
+
* equivalent for its non-deferrable keys; its snapshots are ordered at export.
|
|
1140
|
+
*/
|
|
1141
|
+
export const DEFER_FOREIGN_KEYS_SQL = "PRAGMA defer_foreign_keys = ON;";
|
|
1142
|
+
|
|
843
1143
|
export function buildReplaceSql(siteId) {
|
|
844
1144
|
const statements = [];
|
|
845
1145
|
|
|
@@ -856,6 +1156,41 @@ export function buildReplaceSql(siteId) {
|
|
|
856
1156
|
return statements.join("\n");
|
|
857
1157
|
}
|
|
858
1158
|
|
|
1159
|
+
/** The storage drivers a `media` row can name as its `provider`. */
|
|
1160
|
+
const MEDIA_STORAGE_PROVIDERS = ["r2", "s3", "local"];
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* SQL recording a site's media under the storage an import just wrote it to.
|
|
1164
|
+
*
|
|
1165
|
+
* A snapshot carries each media row's `provider` from the site it was exported
|
|
1166
|
+
* from, but the import uploads every object into the target's own storage. A
|
|
1167
|
+
* row exported on R2 and imported into S3 would still say `r2`, and everything
|
|
1168
|
+
* that reads `provider` as where the bytes live goes wrong: the public URL looks
|
|
1169
|
+
* for `R2_PUBLIC_URL`, replacing the avatar or favicon misses the existing row
|
|
1170
|
+
* and inserts a second one for the same key, and a trashed object is never
|
|
1171
|
+
* purged because the sweep only handles the active driver. Run after the
|
|
1172
|
+
* snapshot's inserts, while every media row of the site is one the snapshot
|
|
1173
|
+
* brought.
|
|
1174
|
+
*
|
|
1175
|
+
* @param {string} siteId - Site whose media was imported
|
|
1176
|
+
* @param {string} provider - Storage driver the import uploaded through
|
|
1177
|
+
* @returns {string} An `UPDATE` statement for the site's media rows
|
|
1178
|
+
* @throws {Error} When `provider` is not one of r2, s3, or local
|
|
1179
|
+
* @example
|
|
1180
|
+
* await execute(
|
|
1181
|
+
* `${buildReplaceSql(site.id)}\n${dbSql}\n${buildMediaProviderSql(site.id, "s3")}`,
|
|
1182
|
+
* );
|
|
1183
|
+
*/
|
|
1184
|
+
export function buildMediaProviderSql(siteId, provider) {
|
|
1185
|
+
if (!MEDIA_STORAGE_PROVIDERS.includes(provider)) {
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
`Snapshot import cannot record media under storage driver "${provider}". Expected one of: ${MEDIA_STORAGE_PROVIDERS.join(", ")}.`,
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
return `UPDATE "media" SET "provider" = '${provider}' WHERE "site_id" = '${escapeSqlString(siteId)}';`;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
859
1194
|
export function normalizeD1Sql(sql) {
|
|
860
1195
|
return sql
|
|
861
1196
|
.replace(/^\s*BEGIN(?:\s+TRANSACTION)?\s*;\s*$/gim, "")
|
package/bin/lib/sql-export.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The order rows are dumped, and so the order they are replayed on import.
|
|
3
|
+
*
|
|
4
|
+
* It is a foreign-key topological sort: a table follows everything it points
|
|
5
|
+
* at. `sortExportTables` applies it to whatever table list it is handed, so
|
|
6
|
+
* this — not the caller's list — is what decides an export's order.
|
|
7
|
+
*
|
|
8
|
+
* A table missing from here is not an ordering no-op. Unknown names sort after
|
|
9
|
+
* every known one, so a new child table lands at the end, behind its own
|
|
10
|
+
* parents, and the dump only fails once there is a row to violate the
|
|
11
|
+
* constraint. `snapshot-tables.test.ts` checks the sorted result, not the
|
|
12
|
+
* caller's list, for that reason.
|
|
13
|
+
*/
|
|
14
|
+
export const TABLE_EXPORT_ORDER = [
|
|
2
15
|
"site",
|
|
3
16
|
"user",
|
|
4
17
|
"account",
|
|
@@ -8,11 +21,15 @@ const TABLE_EXPORT_ORDER = [
|
|
|
8
21
|
"site_setting",
|
|
9
22
|
"site_member",
|
|
10
23
|
"collection",
|
|
11
|
-
|
|
12
|
-
|
|
24
|
+
// Before the three tables that hold a key into it: `nav_item`,
|
|
25
|
+
// `collection_directory_item` and `path_registry`.
|
|
26
|
+
"smart_collection",
|
|
13
27
|
"api_token",
|
|
14
28
|
"post",
|
|
15
29
|
"thread_collection",
|
|
30
|
+
// After `post`: a `page` nav item carries a `post_id`.
|
|
31
|
+
"nav_item",
|
|
32
|
+
"collection_directory_item",
|
|
16
33
|
"path_registry",
|
|
17
34
|
"media",
|
|
18
35
|
];
|
|
@@ -167,8 +184,48 @@ export async function getTableColumns(
|
|
|
167
184
|
.map((row) => String(row.name));
|
|
168
185
|
}
|
|
169
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Read an ordered SELECT a page at a time.
|
|
189
|
+
*
|
|
190
|
+
* `LIMIT … OFFSET …` is appended to the statement, so it must end in an
|
|
191
|
+
* ORDER BY with a unique tiebreaker. Pages are separate reads: rows written
|
|
192
|
+
* between two of them can be skipped or repeated, which is why a snapshot
|
|
193
|
+
* should be taken of a site nobody is writing to.
|
|
194
|
+
*
|
|
195
|
+
* @param {{ query(sql: string): Promise<Record<string, unknown>[]> }} queryRunner
|
|
196
|
+
* @param {string} selectSql - An ordered SELECT with no LIMIT
|
|
197
|
+
* @param {number} pageSize - Rows per read
|
|
198
|
+
* @returns {Promise<Record<string, unknown>[]>} Every row, in order
|
|
199
|
+
* @example
|
|
200
|
+
* await queryAllPages(runner, 'SELECT * FROM "post" ORDER BY rowid', 200);
|
|
201
|
+
*/
|
|
202
|
+
async function queryAllPages(queryRunner, selectSql, pageSize) {
|
|
203
|
+
const base = selectSql.trim();
|
|
204
|
+
const rows = [];
|
|
205
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
206
|
+
const page = await queryRunner.query(
|
|
207
|
+
`${base} LIMIT ${pageSize} OFFSET ${offset}`,
|
|
208
|
+
);
|
|
209
|
+
rows.push(...page);
|
|
210
|
+
if (page.length < pageSize) return rows;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Dump tables as INSERT statements.
|
|
216
|
+
*
|
|
217
|
+
* `options.pageSize` reads each table in pages. D1 needs it: every query runs
|
|
218
|
+
* through `wrangler d1 execute --json`, and one SELECT of a real site's
|
|
219
|
+
* `post` table outgrew the output the CLI can buffer.
|
|
220
|
+
* `options.orderRowsByTable` reorders a table's rows before they are written,
|
|
221
|
+
* for inserts that must follow their own foreign keys.
|
|
222
|
+
*/
|
|
170
223
|
export async function dumpDatabaseToSql(queryRunner, options) {
|
|
171
224
|
const dialect = options.dialect ?? "sqlite";
|
|
225
|
+
const pageSize =
|
|
226
|
+
Number.isInteger(options.pageSize) && options.pageSize > 0
|
|
227
|
+
? options.pageSize
|
|
228
|
+
: null;
|
|
172
229
|
const onProgress =
|
|
173
230
|
typeof options.onProgress === "function" ? options.onProgress : null;
|
|
174
231
|
const configuredTables = Array.isArray(options.tables)
|
|
@@ -194,8 +251,14 @@ export async function dumpDatabaseToSql(queryRunner, options) {
|
|
|
194
251
|
|
|
195
252
|
const selectSql =
|
|
196
253
|
options.selectSqlByTable?.[tableName] ||
|
|
197
|
-
|
|
198
|
-
|
|
254
|
+
(pageSize && dialect === "sqlite"
|
|
255
|
+
? `SELECT * FROM ${quoteIdentifier(tableName)} ORDER BY rowid`
|
|
256
|
+
: `SELECT * FROM ${quoteIdentifier(tableName)}`);
|
|
257
|
+
const readRows = pageSize
|
|
258
|
+
? await queryAllPages(queryRunner, selectSql, pageSize)
|
|
259
|
+
: await queryRunner.query(selectSql);
|
|
260
|
+
const orderRows = options.orderRowsByTable?.[tableName];
|
|
261
|
+
const rows = orderRows ? orderRows(readRows) : readRows;
|
|
199
262
|
if (rows.length === 0) {
|
|
200
263
|
continue;
|
|
201
264
|
}
|
package/bin/lib/wrangler-cli.js
CHANGED
|
@@ -48,12 +48,20 @@ function resolveWranglerBin(cwd = process.cwd()) {
|
|
|
48
48
|
return resolve(dirname(manifestPath), binEntry);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The most output a Wrangler call may print before the CLI gives up on it.
|
|
53
|
+
* `d1 execute --json` prints whole result sets, and Node's default of 1 MiB
|
|
54
|
+
* overflowed on one page of a real site's posts.
|
|
55
|
+
*/
|
|
56
|
+
export const WRANGLER_MAX_BUFFER = 64 * 1024 * 1024;
|
|
57
|
+
|
|
51
58
|
export function runLocalWrangler(args, options = {}) {
|
|
52
59
|
const {
|
|
53
60
|
cwd = process.cwd(),
|
|
54
61
|
encoding = "utf-8",
|
|
55
62
|
env = process.env,
|
|
56
63
|
stdio = "pipe",
|
|
64
|
+
maxBuffer = WRANGLER_MAX_BUFFER,
|
|
57
65
|
...execOptions
|
|
58
66
|
} = options;
|
|
59
67
|
|
|
@@ -63,5 +71,6 @@ export function runLocalWrangler(args, options = {}) {
|
|
|
63
71
|
encoding,
|
|
64
72
|
env,
|
|
65
73
|
stdio,
|
|
74
|
+
maxBuffer,
|
|
66
75
|
});
|
|
67
76
|
}
|