@paroicms/converter 0.1.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/README.md +57 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +74 -0
- package/dist/converter.d.ts +6 -0
- package/dist/converter.js +95 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +198 -0
- package/dist/multisite.d.ts +3 -0
- package/dist/multisite.js +40 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.js +1 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @paroicms/converter
|
|
2
|
+
|
|
3
|
+
CLI tool to migrate field data from Quill format to Tiptap format in ParoiCMS main databases (SQLite).
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### Single Database Mode
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @paroicms/converter \
|
|
11
|
+
--database /path/to/main.sqlite \
|
|
12
|
+
--quill-to-tiptap
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This will:
|
|
16
|
+
|
|
17
|
+
1. Run a dry-run pass to detect errors
|
|
18
|
+
2. Run the actual conversion pass
|
|
19
|
+
|
|
20
|
+
### Multisite Mode
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @paroicms/converter \
|
|
24
|
+
--multisite-data-dir /path/to/data \
|
|
25
|
+
--quill-to-tiptap
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This will process all databases matching the pattern `${dataDir}/*/main.sqlite`:
|
|
29
|
+
|
|
30
|
+
1. Run a dry-run pass on all databases
|
|
31
|
+
2. If all databases pass without errors, run the actual conversion on all databases
|
|
32
|
+
3. If any database has errors in dry-run, abort the entire process
|
|
33
|
+
|
|
34
|
+
## CLI Options
|
|
35
|
+
|
|
36
|
+
- `--database <path>` (required*): Path to the SQLite database file
|
|
37
|
+
- `--multisite-data-dir <path>` (required*): Path to the multisite data directory
|
|
38
|
+
- `--quill-to-tiptap` (required): Activate Quill to Tiptap conversion
|
|
39
|
+
- `--dry-run` (optional): Run in dry-run mode only (no database changes)
|
|
40
|
+
- `--backup` (optional): Create a backup before conversion
|
|
41
|
+
- `--force` (optional): Skip dry-run pass and convert immediately
|
|
42
|
+
|
|
43
|
+
*You must specify either `--database` or `--multisite-data-dir`, but not both.
|
|
44
|
+
|
|
45
|
+
## Exit Codes
|
|
46
|
+
|
|
47
|
+
- `0`: All rows converted successfully
|
|
48
|
+
- `1`: One or more rows failed to convert (errors will be logged)
|
|
49
|
+
|
|
50
|
+
## Error Handling
|
|
51
|
+
|
|
52
|
+
The tool will:
|
|
53
|
+
|
|
54
|
+
- Skip failed rows and continue with others
|
|
55
|
+
- Log all errors with field name, nodeId, and language
|
|
56
|
+
- Return exit code 1 if any errors occurred
|
|
57
|
+
- In multisite mode: abort if any database has errors during dry-run pass
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export function parseCliArguments(args) {
|
|
2
|
+
let database;
|
|
3
|
+
let multisiteDataDir;
|
|
4
|
+
let quillToTiptap = false;
|
|
5
|
+
let dryRun = false;
|
|
6
|
+
let backup = false;
|
|
7
|
+
let force = false;
|
|
8
|
+
for (let i = 0; i < args.length; ++i) {
|
|
9
|
+
const arg = args[i];
|
|
10
|
+
if (arg === "--database") {
|
|
11
|
+
++i;
|
|
12
|
+
if (i >= args.length) {
|
|
13
|
+
throw new Error("Missing value for --database option");
|
|
14
|
+
}
|
|
15
|
+
database = args[i];
|
|
16
|
+
}
|
|
17
|
+
else if (arg === "--multisite-data-dir") {
|
|
18
|
+
++i;
|
|
19
|
+
if (i >= args.length) {
|
|
20
|
+
throw new Error("Missing value for --multisite-data-dir option");
|
|
21
|
+
}
|
|
22
|
+
multisiteDataDir = args[i];
|
|
23
|
+
}
|
|
24
|
+
else if (arg === "--quill-to-tiptap") {
|
|
25
|
+
quillToTiptap = true;
|
|
26
|
+
}
|
|
27
|
+
else if (arg === "--dry-run") {
|
|
28
|
+
dryRun = true;
|
|
29
|
+
}
|
|
30
|
+
else if (arg === "--backup") {
|
|
31
|
+
backup = true;
|
|
32
|
+
}
|
|
33
|
+
else if (arg === "--force") {
|
|
34
|
+
force = true;
|
|
35
|
+
}
|
|
36
|
+
else if (!arg.startsWith("--")) {
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!database && !multisiteDataDir) {
|
|
43
|
+
throw new Error("Missing required option: --database or --multisite-data-dir");
|
|
44
|
+
}
|
|
45
|
+
if (database && multisiteDataDir) {
|
|
46
|
+
throw new Error("Cannot use both --database and --multisite-data-dir options");
|
|
47
|
+
}
|
|
48
|
+
if (!quillToTiptap) {
|
|
49
|
+
throw new Error("Missing required flag: --quill-to-tiptap");
|
|
50
|
+
}
|
|
51
|
+
if (database) {
|
|
52
|
+
const options = {
|
|
53
|
+
kind: "singleDatabase",
|
|
54
|
+
database,
|
|
55
|
+
quillToTiptap,
|
|
56
|
+
dryRun,
|
|
57
|
+
backup,
|
|
58
|
+
force,
|
|
59
|
+
};
|
|
60
|
+
return options;
|
|
61
|
+
}
|
|
62
|
+
if (multisiteDataDir) {
|
|
63
|
+
const options = {
|
|
64
|
+
kind: "multisite",
|
|
65
|
+
multisiteDataDir,
|
|
66
|
+
quillToTiptap,
|
|
67
|
+
dryRun,
|
|
68
|
+
backup,
|
|
69
|
+
force,
|
|
70
|
+
};
|
|
71
|
+
return options;
|
|
72
|
+
}
|
|
73
|
+
throw new Error("Invalid CLI options");
|
|
74
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Knex } from "knex";
|
|
2
|
+
import type { ConversionResult, FieldRow } from "./types.js";
|
|
3
|
+
export declare function createBackup(dbPath: string): Promise<void>;
|
|
4
|
+
export declare function getFieldRows(cn: Knex): Promise<FieldRow[]>;
|
|
5
|
+
export declare function updateRow(cn: Knex, row: FieldRow, tiptapJson: string): Promise<void>;
|
|
6
|
+
export declare function processRows(cn: Knex, rows: FieldRow[], isDryRun: boolean): Promise<ConversionResult>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { convertQuillDeltaToTiptap } from "@paroicms/quill-delta-to-tiptap-json";
|
|
2
|
+
import { copyFile } from "node:fs/promises";
|
|
3
|
+
export async function createBackup(dbPath) {
|
|
4
|
+
const backupPath = `${dbPath}.bak`;
|
|
5
|
+
await copyFile(dbPath, backupPath);
|
|
6
|
+
console.log(`Backup created: ${backupPath}`);
|
|
7
|
+
}
|
|
8
|
+
export async function getFieldRows(cn) {
|
|
9
|
+
const rows = await cn("PaFieldText")
|
|
10
|
+
.select("field", "nodeId", "language", "val")
|
|
11
|
+
.where("plugin", "@paroicms/quill-editor-plugin");
|
|
12
|
+
return rows;
|
|
13
|
+
}
|
|
14
|
+
function convertRow(row) {
|
|
15
|
+
try {
|
|
16
|
+
const quillDelta = JSON.parse(row.val);
|
|
17
|
+
const converterOptions = {
|
|
18
|
+
customConverters: {
|
|
19
|
+
inlineFormats: {
|
|
20
|
+
size: (value) => ({ type: "textSize", attrs: { value } }),
|
|
21
|
+
obfuscate: (value) => ({ type: "obfuscate", attrs: { asALink: value } }),
|
|
22
|
+
"internal-link-plugin": (value) => ({
|
|
23
|
+
type: "internalLinkPlugin",
|
|
24
|
+
attrs: { documentId: value },
|
|
25
|
+
}),
|
|
26
|
+
underline: (value) => (value ? { type: "italic" } : undefined),
|
|
27
|
+
},
|
|
28
|
+
embeds: {
|
|
29
|
+
media: (value) => {
|
|
30
|
+
const attrs = { ...value };
|
|
31
|
+
if (attrs.zoomable === undefined || attrs.zoomable === true) {
|
|
32
|
+
attrs.zoomable = true;
|
|
33
|
+
}
|
|
34
|
+
if (attrs.zoomable === false) {
|
|
35
|
+
delete attrs.zoomable;
|
|
36
|
+
}
|
|
37
|
+
return { type: "media", attrs };
|
|
38
|
+
},
|
|
39
|
+
"html-snippet": (value) => ({ type: "htmlSnippet", attrs: { html: value } }),
|
|
40
|
+
"video-plugin": (value) => ({ type: "videoPlugin", attrs: { videoId: value } }),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const conversionResult = convertQuillDeltaToTiptap(quillDelta, converterOptions);
|
|
45
|
+
const tiptapJson = JSON.stringify(conversionResult.result);
|
|
46
|
+
return { success: true, tiptapJson };
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
50
|
+
return { success: false, error: errorMessage };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function updateRow(cn, row, tiptapJson) {
|
|
54
|
+
await cn("PaFieldText")
|
|
55
|
+
.update({
|
|
56
|
+
val: tiptapJson,
|
|
57
|
+
plugin: "@paroicms/tiptap-editor-plugin",
|
|
58
|
+
})
|
|
59
|
+
.where({
|
|
60
|
+
field: row.field,
|
|
61
|
+
nodeId: row.nodeId,
|
|
62
|
+
language: row.language,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export async function processRows(cn, rows, isDryRun) {
|
|
66
|
+
const result = {
|
|
67
|
+
totalRows: rows.length,
|
|
68
|
+
converted: 0,
|
|
69
|
+
errors: 0,
|
|
70
|
+
errorDetails: [],
|
|
71
|
+
};
|
|
72
|
+
for (let i = 0; i < rows.length; ++i) {
|
|
73
|
+
const row = rows[i];
|
|
74
|
+
if ((i + 1) % 50 === 0 || i + 1 === rows.length) {
|
|
75
|
+
console.log(`Progress: ${i + 1}/${rows.length}...`);
|
|
76
|
+
}
|
|
77
|
+
const conversionResult = convertRow(row);
|
|
78
|
+
if (conversionResult.success) {
|
|
79
|
+
++result.converted;
|
|
80
|
+
if (!isDryRun && conversionResult.tiptapJson) {
|
|
81
|
+
await updateRow(cn, row, conversionResult.tiptapJson);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
++result.errors;
|
|
86
|
+
result.errorDetails.push({
|
|
87
|
+
field: row.field,
|
|
88
|
+
nodeId: row.nodeId,
|
|
89
|
+
language: row.language,
|
|
90
|
+
error: conversionResult.error ?? "Unknown error",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import knex from "knex";
|
|
3
|
+
import { access } from "node:fs/promises";
|
|
4
|
+
import { parseCliArguments } from "./cli.js";
|
|
5
|
+
import { createBackup, getFieldRows, processRows } from "./converter.js";
|
|
6
|
+
import { createMultisiteResult, discoverDatabases } from "./multisite.js";
|
|
7
|
+
async function processSingleDatabase(dbPath, isDryRun, doBackup) {
|
|
8
|
+
if (doBackup && !isDryRun) {
|
|
9
|
+
await createBackup(dbPath);
|
|
10
|
+
}
|
|
11
|
+
const cn = knex({
|
|
12
|
+
client: "sqlite3",
|
|
13
|
+
connection: {
|
|
14
|
+
filename: dbPath,
|
|
15
|
+
},
|
|
16
|
+
useNullAsDefault: true,
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
const rows = await getFieldRows(cn);
|
|
20
|
+
console.log(`Found ${rows.length} rows to process\n`);
|
|
21
|
+
if (rows.length === 0) {
|
|
22
|
+
return {
|
|
23
|
+
totalRows: 0,
|
|
24
|
+
converted: 0,
|
|
25
|
+
errors: 0,
|
|
26
|
+
errorDetails: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
console.log(`Processing ${rows.length} rows...`);
|
|
30
|
+
return await processRows(cn, rows, isDryRun);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
await cn.destroy();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function runSingleDatabaseMode() {
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
console.log("Field Converter: Quill to Tiptap");
|
|
39
|
+
const options = parseCliArguments(process.argv.slice(2));
|
|
40
|
+
if (options.kind !== "singleDatabase") {
|
|
41
|
+
throw new Error("Invalid options kind");
|
|
42
|
+
}
|
|
43
|
+
console.log(`Database: ${options.database}`);
|
|
44
|
+
let exitCode = 0;
|
|
45
|
+
if (!options.force) {
|
|
46
|
+
console.log("Mode: dry-run (first pass)");
|
|
47
|
+
const dryRunResult = await processSingleDatabase(options.database, true, false);
|
|
48
|
+
console.log(`Converted: ${dryRunResult.converted} | Errors: ${dryRunResult.errors}\n`);
|
|
49
|
+
if (dryRunResult.errors > 0) {
|
|
50
|
+
console.log("Errors:");
|
|
51
|
+
for (const errorDetail of dryRunResult.errorDetails) {
|
|
52
|
+
console.log(`- Row [field=${errorDetail.field}, nodeId=${errorDetail.nodeId}, language=${errorDetail.language}]: ${errorDetail.error}`);
|
|
53
|
+
}
|
|
54
|
+
console.log();
|
|
55
|
+
exitCode = 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
console.log("Mode: conversion (second pass)");
|
|
59
|
+
const conversionResult = await processSingleDatabase(options.database, false, options.backup ?? false);
|
|
60
|
+
console.log(`Converted: ${conversionResult.converted} | Errors: ${conversionResult.errors}\n`);
|
|
61
|
+
if (conversionResult.errors > 0) {
|
|
62
|
+
console.log("Errors:");
|
|
63
|
+
for (const errorDetail of conversionResult.errorDetails) {
|
|
64
|
+
console.log(`- Row [field=${errorDetail.field}, nodeId=${errorDetail.nodeId}, language=${errorDetail.language}]: ${errorDetail.error}`);
|
|
65
|
+
}
|
|
66
|
+
console.log();
|
|
67
|
+
exitCode = 1;
|
|
68
|
+
}
|
|
69
|
+
const totalTimeInSeconds = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
70
|
+
console.log(`Total time: ${totalTimeInSeconds}s\n`);
|
|
71
|
+
if (exitCode === 1) {
|
|
72
|
+
console.log("Exit code: 1 (errors occurred)");
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log("Exit code: 0");
|
|
76
|
+
}
|
|
77
|
+
process.exit(exitCode);
|
|
78
|
+
}
|
|
79
|
+
async function runMultisiteMode() {
|
|
80
|
+
const startTime = Date.now();
|
|
81
|
+
console.log("Field Converter: Quill to Tiptap (Multisite Mode)");
|
|
82
|
+
const options = parseCliArguments(process.argv.slice(2));
|
|
83
|
+
if (options.kind !== "multisite") {
|
|
84
|
+
throw new Error("Invalid options kind");
|
|
85
|
+
}
|
|
86
|
+
console.log(`Multisite data directory: ${options.multisiteDataDir}\n`);
|
|
87
|
+
const databases = await discoverDatabases(options.multisiteDataDir);
|
|
88
|
+
console.log(`Discovered ${databases.length} site(s)\n`);
|
|
89
|
+
if (databases.length === 0) {
|
|
90
|
+
console.log("No databases found. Exit code: 0");
|
|
91
|
+
process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
// Filter databases that exist
|
|
94
|
+
const existingDatabases = [];
|
|
95
|
+
for (const db of databases) {
|
|
96
|
+
try {
|
|
97
|
+
await access(db.dbPath);
|
|
98
|
+
existingDatabases.push(db);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
console.log(`⚠️ Skipping ${db.siteName} - main.sqlite not found`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (existingDatabases.length === 0) {
|
|
105
|
+
console.log("No databases found. Exit code: 0");
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
console.log(`Processing ${existingDatabases.length} database(s)\n`);
|
|
109
|
+
// First pass: dry-run on all databases (unless force mode)
|
|
110
|
+
if (!options.force) {
|
|
111
|
+
console.log("=".repeat(80));
|
|
112
|
+
console.log("DRY-RUN PASS - Checking all databases");
|
|
113
|
+
console.log("=".repeat(80) + "\n");
|
|
114
|
+
for (const db of existingDatabases) {
|
|
115
|
+
console.log(`\n--- ${db.siteName} ---`);
|
|
116
|
+
console.log(`Database: ${db.dbPath}`);
|
|
117
|
+
db.result = await processSingleDatabase(db.dbPath, true, false);
|
|
118
|
+
console.log(`Converted: ${db.result.converted} | Errors: ${db.result.errors}`);
|
|
119
|
+
if (db.result.errors > 0) {
|
|
120
|
+
console.log("Errors:");
|
|
121
|
+
for (const errorDetail of db.result.errorDetails) {
|
|
122
|
+
console.log(`- Row [field=${errorDetail.field}, nodeId=${errorDetail.nodeId}, language=${errorDetail.language}]: ${errorDetail.error}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const multisiteResult = createMultisiteResult(existingDatabases);
|
|
127
|
+
const totalTimeInSeconds = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
128
|
+
console.log(`\n${"=".repeat(80)}`);
|
|
129
|
+
console.log("DRY-RUN SUMMARY");
|
|
130
|
+
console.log("=".repeat(80));
|
|
131
|
+
console.log(`Total databases: ${multisiteResult.totalDatabases}`);
|
|
132
|
+
console.log(`Databases with errors: ${multisiteResult.databasesWithErrors}`);
|
|
133
|
+
console.log(`Successful databases: ${multisiteResult.successfulDatabases}`);
|
|
134
|
+
console.log(`Total time: ${totalTimeInSeconds}s\n`);
|
|
135
|
+
if (multisiteResult.databasesWithErrors > 0) {
|
|
136
|
+
console.log("❌ Errors detected in dry-run pass. Aborting conversion.");
|
|
137
|
+
console.log("Exit code: 1 (errors occurred)");
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
if (options.dryRun) {
|
|
141
|
+
console.log("✅ Dry-run completed successfully.");
|
|
142
|
+
console.log("Exit code: 0");
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
console.log("✅ Dry-run pass completed with no errors. Proceeding with conversion.\n");
|
|
146
|
+
}
|
|
147
|
+
// Second pass: actual conversion on all databases
|
|
148
|
+
console.log("=".repeat(80));
|
|
149
|
+
console.log("CONVERSION PASS - Converting all databases");
|
|
150
|
+
console.log("=".repeat(80) + "\n");
|
|
151
|
+
for (const db of existingDatabases) {
|
|
152
|
+
console.log(`\n--- ${db.siteName} ---`);
|
|
153
|
+
console.log(`Database: ${db.dbPath}`);
|
|
154
|
+
db.result = await processSingleDatabase(db.dbPath, false, options.backup ?? false);
|
|
155
|
+
console.log(`Converted: ${db.result.converted} | Errors: ${db.result.errors}`);
|
|
156
|
+
if (db.result.errors > 0) {
|
|
157
|
+
console.log("Errors:");
|
|
158
|
+
for (const errorDetail of db.result.errorDetails) {
|
|
159
|
+
console.log(`- Row [field=${errorDetail.field}, nodeId=${errorDetail.nodeId}, language=${errorDetail.language}]: ${errorDetail.error}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const multisiteResult = createMultisiteResult(existingDatabases);
|
|
164
|
+
const totalTimeInSeconds = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
165
|
+
console.log("\n" + "=".repeat(80));
|
|
166
|
+
console.log("FINAL SUMMARY");
|
|
167
|
+
console.log("=".repeat(80));
|
|
168
|
+
console.log(`Total databases: ${multisiteResult.totalDatabases}`);
|
|
169
|
+
console.log(`Successful databases: ${multisiteResult.successfulDatabases}`);
|
|
170
|
+
console.log(`Databases with errors: ${multisiteResult.databasesWithErrors}`);
|
|
171
|
+
console.log(`Total time: ${totalTimeInSeconds}s\n`);
|
|
172
|
+
if (multisiteResult.databasesWithErrors > 0) {
|
|
173
|
+
console.log("Exit code: 1 (errors occurred)");
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
console.log("Exit code: 0");
|
|
177
|
+
process.exit(0);
|
|
178
|
+
}
|
|
179
|
+
async function main() {
|
|
180
|
+
try {
|
|
181
|
+
const options = parseCliArguments(process.argv.slice(2));
|
|
182
|
+
if (options.kind === "singleDatabase") {
|
|
183
|
+
await runSingleDatabaseMode();
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
await runMultisiteMode();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
191
|
+
console.error(`Error: ${errorMessage}`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
main().catch((error) => {
|
|
196
|
+
console.error("Unhandled error:", error);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { DatabaseResult, MultisiteConversionResult } from "./types.js";
|
|
2
|
+
export declare function discoverDatabases(multisiteDataDir: string): Promise<DatabaseResult[]>;
|
|
3
|
+
export declare function createMultisiteResult(databases: DatabaseResult[]): MultisiteConversionResult;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export async function discoverDatabases(multisiteDataDir) {
|
|
4
|
+
const entries = await readdir(multisiteDataDir, { withFileTypes: true });
|
|
5
|
+
const siteDirs = entries.filter((entry) => entry.isDirectory());
|
|
6
|
+
const databases = [];
|
|
7
|
+
for (const siteDir of siteDirs) {
|
|
8
|
+
const siteName = siteDir.name;
|
|
9
|
+
const dbPath = join(multisiteDataDir, siteName, "main.sqlite");
|
|
10
|
+
databases.push({
|
|
11
|
+
siteName,
|
|
12
|
+
dbPath,
|
|
13
|
+
result: {
|
|
14
|
+
totalRows: 0,
|
|
15
|
+
converted: 0,
|
|
16
|
+
errors: 0,
|
|
17
|
+
errorDetails: [],
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return databases;
|
|
22
|
+
}
|
|
23
|
+
export function createMultisiteResult(databases) {
|
|
24
|
+
let successfulDatabases = 0;
|
|
25
|
+
let databasesWithErrors = 0;
|
|
26
|
+
for (const db of databases) {
|
|
27
|
+
if (db.result.errors === 0 && db.result.totalRows > 0) {
|
|
28
|
+
++successfulDatabases;
|
|
29
|
+
}
|
|
30
|
+
else if (db.result.errors > 0) {
|
|
31
|
+
++databasesWithErrors;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
totalDatabases: databases.length,
|
|
36
|
+
successfulDatabases,
|
|
37
|
+
databasesWithErrors,
|
|
38
|
+
databases,
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type CliOptions = SingleDatabaseCliOptions | MultisiteDatabaseCliOptions;
|
|
2
|
+
export interface SingleDatabaseCliOptions {
|
|
3
|
+
kind: "singleDatabase";
|
|
4
|
+
database: string;
|
|
5
|
+
quillToTiptap: boolean;
|
|
6
|
+
dryRun?: boolean;
|
|
7
|
+
backup?: boolean;
|
|
8
|
+
force?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface MultisiteDatabaseCliOptions {
|
|
11
|
+
kind: "multisite";
|
|
12
|
+
multisiteDataDir: string;
|
|
13
|
+
quillToTiptap: boolean;
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
backup?: boolean;
|
|
16
|
+
force?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface ConversionResult {
|
|
19
|
+
totalRows: number;
|
|
20
|
+
converted: number;
|
|
21
|
+
errors: number;
|
|
22
|
+
errorDetails: Array<{
|
|
23
|
+
field: string;
|
|
24
|
+
nodeId: number;
|
|
25
|
+
language: string;
|
|
26
|
+
error: string;
|
|
27
|
+
}>;
|
|
28
|
+
}
|
|
29
|
+
export interface FieldRow {
|
|
30
|
+
field: string;
|
|
31
|
+
nodeId: number;
|
|
32
|
+
language: string;
|
|
33
|
+
val: string;
|
|
34
|
+
}
|
|
35
|
+
export interface DatabaseResult {
|
|
36
|
+
siteName: string;
|
|
37
|
+
dbPath: string;
|
|
38
|
+
result: ConversionResult;
|
|
39
|
+
}
|
|
40
|
+
export interface MultisiteConversionResult {
|
|
41
|
+
totalDatabases: number;
|
|
42
|
+
successfulDatabases: number;
|
|
43
|
+
databasesWithErrors: number;
|
|
44
|
+
databases: DatabaseResult[];
|
|
45
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@paroicms/converter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI tool to migrate field data from Quill to Tiptap format",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"paroicms-converter": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"clear": "rimraf dist/*",
|
|
12
|
+
"dev": "tsc --watch --preserveWatchOutput",
|
|
13
|
+
"postbuild": "chmod +x dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"knex": "~3.1.0",
|
|
17
|
+
"sqlite3": "~5.1.7",
|
|
18
|
+
"@paroicms/quill-delta-to-tiptap-json": "0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "~24.8.1",
|
|
22
|
+
"rimraf": "~6.0.1",
|
|
23
|
+
"typescript": "~5.9.3"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
]
|
|
28
|
+
}
|