@njdamstra/appwrite-utils-cli 1.10.1 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/migrateCommands.d.ts +6 -0
- package/dist/cli/commands/migrateCommands.js +118 -0
- package/dist/interactiveCLI.js +6 -0
- package/dist/main.js +56 -0
- package/dist/migrations/migrateStrings.d.ts +9 -0
- package/dist/migrations/migrateStrings.js +724 -0
- package/dist/migrations/migrateStringsTypes.d.ts +195 -0
- package/dist/migrations/migrateStringsTypes.js +117 -0
- package/package.json +1 -1
- package/src/cli/commands/migrateCommands.ts +157 -0
- package/src/interactiveCLI.ts +6 -0
- package/src/main.ts +69 -0
- package/src/migrations/migrateStrings.ts +1064 -0
- package/src/migrations/migrateStringsTypes.ts +158 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const MigrationTargetType: z.ZodEnum<{
|
|
3
|
+
varchar: "varchar";
|
|
4
|
+
text: "text";
|
|
5
|
+
mediumtext: "mediumtext";
|
|
6
|
+
longtext: "longtext";
|
|
7
|
+
}>;
|
|
8
|
+
export type MigrationTargetType = z.infer<typeof MigrationTargetType>;
|
|
9
|
+
export declare const MigrationAction: z.ZodEnum<{
|
|
10
|
+
skip: "skip";
|
|
11
|
+
migrate: "migrate";
|
|
12
|
+
}>;
|
|
13
|
+
export type MigrationAction = z.infer<typeof MigrationAction>;
|
|
14
|
+
export declare const MigrationPlanEntrySchema: z.ZodObject<{
|
|
15
|
+
databaseId: z.ZodString;
|
|
16
|
+
databaseName: z.ZodString;
|
|
17
|
+
collectionId: z.ZodString;
|
|
18
|
+
collectionName: z.ZodString;
|
|
19
|
+
attributeKey: z.ZodString;
|
|
20
|
+
currentType: z.ZodDefault<z.ZodString>;
|
|
21
|
+
currentSize: z.ZodNumber;
|
|
22
|
+
isRequired: z.ZodDefault<z.ZodBoolean>;
|
|
23
|
+
isArray: z.ZodDefault<z.ZodBoolean>;
|
|
24
|
+
isEncrypted: z.ZodDefault<z.ZodBoolean>;
|
|
25
|
+
hasDefault: z.ZodDefault<z.ZodBoolean>;
|
|
26
|
+
defaultValue: z.ZodOptional<z.ZodAny>;
|
|
27
|
+
suggestedType: z.ZodEnum<{
|
|
28
|
+
varchar: "varchar";
|
|
29
|
+
text: "text";
|
|
30
|
+
mediumtext: "mediumtext";
|
|
31
|
+
longtext: "longtext";
|
|
32
|
+
}>;
|
|
33
|
+
targetType: z.ZodEnum<{
|
|
34
|
+
varchar: "varchar";
|
|
35
|
+
text: "text";
|
|
36
|
+
mediumtext: "mediumtext";
|
|
37
|
+
longtext: "longtext";
|
|
38
|
+
}>;
|
|
39
|
+
targetSize: z.ZodOptional<z.ZodNumber>;
|
|
40
|
+
action: z.ZodEnum<{
|
|
41
|
+
skip: "skip";
|
|
42
|
+
migrate: "migrate";
|
|
43
|
+
}>;
|
|
44
|
+
skipReason: z.ZodOptional<z.ZodString>;
|
|
45
|
+
indexesAffected: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
export type MigrationPlanEntry = z.infer<typeof MigrationPlanEntrySchema>;
|
|
48
|
+
export declare const MigrationPlanSchema: z.ZodObject<{
|
|
49
|
+
version: z.ZodDefault<z.ZodNumber>;
|
|
50
|
+
generatedAt: z.ZodString;
|
|
51
|
+
appwriteEndpoint: z.ZodOptional<z.ZodString>;
|
|
52
|
+
appwriteProject: z.ZodOptional<z.ZodString>;
|
|
53
|
+
summary: z.ZodObject<{
|
|
54
|
+
totalStringAttributes: z.ZodNumber;
|
|
55
|
+
toMigrate: z.ZodNumber;
|
|
56
|
+
toSkip: z.ZodNumber;
|
|
57
|
+
databaseCount: z.ZodNumber;
|
|
58
|
+
collectionCount: z.ZodNumber;
|
|
59
|
+
}, z.core.$strip>;
|
|
60
|
+
entries: z.ZodArray<z.ZodObject<{
|
|
61
|
+
databaseId: z.ZodString;
|
|
62
|
+
databaseName: z.ZodString;
|
|
63
|
+
collectionId: z.ZodString;
|
|
64
|
+
collectionName: z.ZodString;
|
|
65
|
+
attributeKey: z.ZodString;
|
|
66
|
+
currentType: z.ZodDefault<z.ZodString>;
|
|
67
|
+
currentSize: z.ZodNumber;
|
|
68
|
+
isRequired: z.ZodDefault<z.ZodBoolean>;
|
|
69
|
+
isArray: z.ZodDefault<z.ZodBoolean>;
|
|
70
|
+
isEncrypted: z.ZodDefault<z.ZodBoolean>;
|
|
71
|
+
hasDefault: z.ZodDefault<z.ZodBoolean>;
|
|
72
|
+
defaultValue: z.ZodOptional<z.ZodAny>;
|
|
73
|
+
suggestedType: z.ZodEnum<{
|
|
74
|
+
varchar: "varchar";
|
|
75
|
+
text: "text";
|
|
76
|
+
mediumtext: "mediumtext";
|
|
77
|
+
longtext: "longtext";
|
|
78
|
+
}>;
|
|
79
|
+
targetType: z.ZodEnum<{
|
|
80
|
+
varchar: "varchar";
|
|
81
|
+
text: "text";
|
|
82
|
+
mediumtext: "mediumtext";
|
|
83
|
+
longtext: "longtext";
|
|
84
|
+
}>;
|
|
85
|
+
targetSize: z.ZodOptional<z.ZodNumber>;
|
|
86
|
+
action: z.ZodEnum<{
|
|
87
|
+
skip: "skip";
|
|
88
|
+
migrate: "migrate";
|
|
89
|
+
}>;
|
|
90
|
+
skipReason: z.ZodOptional<z.ZodString>;
|
|
91
|
+
indexesAffected: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
92
|
+
}, z.core.$strip>>;
|
|
93
|
+
}, z.core.$strip>;
|
|
94
|
+
export type MigrationPlan = z.infer<typeof MigrationPlanSchema>;
|
|
95
|
+
export declare const CheckpointPhase: z.ZodEnum<{
|
|
96
|
+
pending: "pending";
|
|
97
|
+
completed: "completed";
|
|
98
|
+
failed: "failed";
|
|
99
|
+
backup_created: "backup_created";
|
|
100
|
+
data_copied_to_backup: "data_copied_to_backup";
|
|
101
|
+
data_verified_backup: "data_verified_backup";
|
|
102
|
+
original_deleted: "original_deleted";
|
|
103
|
+
new_attr_created: "new_attr_created";
|
|
104
|
+
data_copied_back: "data_copied_back";
|
|
105
|
+
data_verified_final: "data_verified_final";
|
|
106
|
+
backup_deleted: "backup_deleted";
|
|
107
|
+
}>;
|
|
108
|
+
export type CheckpointPhase = z.infer<typeof CheckpointPhase>;
|
|
109
|
+
export declare const CheckpointEntrySchema: z.ZodObject<{
|
|
110
|
+
databaseId: z.ZodString;
|
|
111
|
+
collectionId: z.ZodString;
|
|
112
|
+
attributeKey: z.ZodString;
|
|
113
|
+
backupKey: z.ZodString;
|
|
114
|
+
phase: z.ZodEnum<{
|
|
115
|
+
pending: "pending";
|
|
116
|
+
completed: "completed";
|
|
117
|
+
failed: "failed";
|
|
118
|
+
backup_created: "backup_created";
|
|
119
|
+
data_copied_to_backup: "data_copied_to_backup";
|
|
120
|
+
data_verified_backup: "data_verified_backup";
|
|
121
|
+
original_deleted: "original_deleted";
|
|
122
|
+
new_attr_created: "new_attr_created";
|
|
123
|
+
data_copied_back: "data_copied_back";
|
|
124
|
+
data_verified_final: "data_verified_final";
|
|
125
|
+
backup_deleted: "backup_deleted";
|
|
126
|
+
}>;
|
|
127
|
+
targetType: z.ZodEnum<{
|
|
128
|
+
varchar: "varchar";
|
|
129
|
+
text: "text";
|
|
130
|
+
mediumtext: "mediumtext";
|
|
131
|
+
longtext: "longtext";
|
|
132
|
+
}>;
|
|
133
|
+
targetSize: z.ZodOptional<z.ZodNumber>;
|
|
134
|
+
error: z.ZodOptional<z.ZodString>;
|
|
135
|
+
storedIndexes: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
136
|
+
key: z.ZodString;
|
|
137
|
+
type: z.ZodString;
|
|
138
|
+
attributes: z.ZodArray<z.ZodString>;
|
|
139
|
+
orders: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
140
|
+
}, z.core.$strip>>>;
|
|
141
|
+
}, z.core.$strip>;
|
|
142
|
+
export type CheckpointEntry = z.infer<typeof CheckpointEntrySchema>;
|
|
143
|
+
export declare const MigrationCheckpointSchema: z.ZodObject<{
|
|
144
|
+
planFile: z.ZodString;
|
|
145
|
+
startedAt: z.ZodString;
|
|
146
|
+
lastUpdatedAt: z.ZodString;
|
|
147
|
+
entries: z.ZodArray<z.ZodObject<{
|
|
148
|
+
databaseId: z.ZodString;
|
|
149
|
+
collectionId: z.ZodString;
|
|
150
|
+
attributeKey: z.ZodString;
|
|
151
|
+
backupKey: z.ZodString;
|
|
152
|
+
phase: z.ZodEnum<{
|
|
153
|
+
pending: "pending";
|
|
154
|
+
completed: "completed";
|
|
155
|
+
failed: "failed";
|
|
156
|
+
backup_created: "backup_created";
|
|
157
|
+
data_copied_to_backup: "data_copied_to_backup";
|
|
158
|
+
data_verified_backup: "data_verified_backup";
|
|
159
|
+
original_deleted: "original_deleted";
|
|
160
|
+
new_attr_created: "new_attr_created";
|
|
161
|
+
data_copied_back: "data_copied_back";
|
|
162
|
+
data_verified_final: "data_verified_final";
|
|
163
|
+
backup_deleted: "backup_deleted";
|
|
164
|
+
}>;
|
|
165
|
+
targetType: z.ZodEnum<{
|
|
166
|
+
varchar: "varchar";
|
|
167
|
+
text: "text";
|
|
168
|
+
mediumtext: "mediumtext";
|
|
169
|
+
longtext: "longtext";
|
|
170
|
+
}>;
|
|
171
|
+
targetSize: z.ZodOptional<z.ZodNumber>;
|
|
172
|
+
error: z.ZodOptional<z.ZodString>;
|
|
173
|
+
storedIndexes: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
174
|
+
key: z.ZodString;
|
|
175
|
+
type: z.ZodString;
|
|
176
|
+
attributes: z.ZodArray<z.ZodString>;
|
|
177
|
+
orders: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
178
|
+
}, z.core.$strip>>>;
|
|
179
|
+
}, z.core.$strip>>;
|
|
180
|
+
}, z.core.$strip>;
|
|
181
|
+
export type MigrationCheckpoint = z.infer<typeof MigrationCheckpointSchema>;
|
|
182
|
+
export interface AnalyzeOptions {
|
|
183
|
+
outputPath?: string;
|
|
184
|
+
verbose?: boolean;
|
|
185
|
+
}
|
|
186
|
+
export interface ExecuteOptions {
|
|
187
|
+
planPath: string;
|
|
188
|
+
keepBackups?: boolean;
|
|
189
|
+
dryRun?: boolean;
|
|
190
|
+
batchSize?: number;
|
|
191
|
+
batchDelayMs?: number;
|
|
192
|
+
checkpointPath?: string;
|
|
193
|
+
}
|
|
194
|
+
export declare function suggestTargetType(size: number, hasIndex: boolean): MigrationTargetType;
|
|
195
|
+
export declare function generateBackupKey(originalKey: string): string;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// ── Target types for string attribute migration ──
|
|
3
|
+
export const MigrationTargetType = z.enum([
|
|
4
|
+
"varchar",
|
|
5
|
+
"text",
|
|
6
|
+
"mediumtext",
|
|
7
|
+
"longtext",
|
|
8
|
+
]);
|
|
9
|
+
export const MigrationAction = z.enum(["migrate", "skip"]);
|
|
10
|
+
// ── Plan entry: one per attribute to migrate ──
|
|
11
|
+
export const MigrationPlanEntrySchema = z.object({
|
|
12
|
+
databaseId: z.string(),
|
|
13
|
+
databaseName: z.string(),
|
|
14
|
+
collectionId: z.string(),
|
|
15
|
+
collectionName: z.string(),
|
|
16
|
+
attributeKey: z.string(),
|
|
17
|
+
currentType: z.string().default("string"),
|
|
18
|
+
currentSize: z.number(),
|
|
19
|
+
isRequired: z.boolean().default(false),
|
|
20
|
+
isArray: z.boolean().default(false),
|
|
21
|
+
isEncrypted: z.boolean().default(false),
|
|
22
|
+
hasDefault: z.boolean().default(false),
|
|
23
|
+
defaultValue: z.any().optional(),
|
|
24
|
+
suggestedType: MigrationTargetType,
|
|
25
|
+
targetType: MigrationTargetType,
|
|
26
|
+
targetSize: z.number().optional(), // varchar only
|
|
27
|
+
action: MigrationAction,
|
|
28
|
+
skipReason: z.string().optional(),
|
|
29
|
+
indexesAffected: z.array(z.string()).default([]),
|
|
30
|
+
});
|
|
31
|
+
// ── Plan: full migration plan (user-editable YAML) ──
|
|
32
|
+
export const MigrationPlanSchema = z.object({
|
|
33
|
+
version: z.number().default(1),
|
|
34
|
+
generatedAt: z.string(),
|
|
35
|
+
appwriteEndpoint: z.string().optional(),
|
|
36
|
+
appwriteProject: z.string().optional(),
|
|
37
|
+
summary: z.object({
|
|
38
|
+
totalStringAttributes: z.number(),
|
|
39
|
+
toMigrate: z.number(),
|
|
40
|
+
toSkip: z.number(),
|
|
41
|
+
databaseCount: z.number(),
|
|
42
|
+
collectionCount: z.number(),
|
|
43
|
+
}),
|
|
44
|
+
entries: z.array(MigrationPlanEntrySchema),
|
|
45
|
+
});
|
|
46
|
+
// ── Checkpoint: tracks progress during execution ──
|
|
47
|
+
export const CheckpointPhase = z.enum([
|
|
48
|
+
"pending",
|
|
49
|
+
"backup_created",
|
|
50
|
+
"data_copied_to_backup",
|
|
51
|
+
"data_verified_backup",
|
|
52
|
+
"original_deleted",
|
|
53
|
+
"new_attr_created",
|
|
54
|
+
"data_copied_back",
|
|
55
|
+
"data_verified_final",
|
|
56
|
+
"backup_deleted",
|
|
57
|
+
"completed",
|
|
58
|
+
"failed",
|
|
59
|
+
]);
|
|
60
|
+
export const CheckpointEntrySchema = z.object({
|
|
61
|
+
databaseId: z.string(),
|
|
62
|
+
collectionId: z.string(),
|
|
63
|
+
attributeKey: z.string(),
|
|
64
|
+
backupKey: z.string(),
|
|
65
|
+
phase: CheckpointPhase,
|
|
66
|
+
targetType: MigrationTargetType,
|
|
67
|
+
targetSize: z.number().optional(),
|
|
68
|
+
error: z.string().optional(),
|
|
69
|
+
// Store index definitions for recreation after attribute delete
|
|
70
|
+
storedIndexes: z
|
|
71
|
+
.array(z.object({
|
|
72
|
+
key: z.string(),
|
|
73
|
+
type: z.string(),
|
|
74
|
+
attributes: z.array(z.string()),
|
|
75
|
+
orders: z.array(z.string()).optional(),
|
|
76
|
+
}))
|
|
77
|
+
.default([]),
|
|
78
|
+
});
|
|
79
|
+
export const MigrationCheckpointSchema = z.object({
|
|
80
|
+
planFile: z.string(),
|
|
81
|
+
startedAt: z.string(),
|
|
82
|
+
lastUpdatedAt: z.string(),
|
|
83
|
+
entries: z.array(CheckpointEntrySchema),
|
|
84
|
+
});
|
|
85
|
+
// ── Helper: suggest target type from size + index presence ──
|
|
86
|
+
export function suggestTargetType(size, hasIndex) {
|
|
87
|
+
// varchar supports up to 16,383 bytes and allows full indexing
|
|
88
|
+
if (size <= 768)
|
|
89
|
+
return "varchar";
|
|
90
|
+
if (hasIndex)
|
|
91
|
+
return "varchar"; // indexes require varchar
|
|
92
|
+
if (size <= 16_383)
|
|
93
|
+
return "text";
|
|
94
|
+
if (size <= 4_000_000)
|
|
95
|
+
return "mediumtext";
|
|
96
|
+
return "longtext";
|
|
97
|
+
}
|
|
98
|
+
// ── Helper: generate backup key with length limits ──
|
|
99
|
+
const MAX_KEY_LENGTH = 36;
|
|
100
|
+
const BACKUP_PREFIX = "_mig_";
|
|
101
|
+
export function generateBackupKey(originalKey) {
|
|
102
|
+
const candidate = `${BACKUP_PREFIX}${originalKey}`;
|
|
103
|
+
if (candidate.length <= MAX_KEY_LENGTH) {
|
|
104
|
+
return candidate;
|
|
105
|
+
}
|
|
106
|
+
// Truncate + 4-char hash for uniqueness
|
|
107
|
+
const hash = simpleHash(originalKey);
|
|
108
|
+
const maxOrigLen = MAX_KEY_LENGTH - BACKUP_PREFIX.length - 1 - 4; // _mig_ + _ + hash4
|
|
109
|
+
return `_m_${originalKey.slice(0, maxOrigLen)}_${hash}`;
|
|
110
|
+
}
|
|
111
|
+
function simpleHash(str) {
|
|
112
|
+
let h = 0;
|
|
113
|
+
for (let i = 0; i < str.length; i++) {
|
|
114
|
+
h = ((h << 5) - h + str.charCodeAt(i)) | 0;
|
|
115
|
+
}
|
|
116
|
+
return Math.abs(h).toString(36).slice(0, 4).padStart(4, "0");
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@njdamstra/appwrite-utils-cli",
|
|
3
3
|
"description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.11.0",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MessageFormatter } from "@njdamstra/appwrite-utils-helpers";
|
|
4
|
+
import type { InteractiveCLI } from "../../interactiveCLI.js";
|
|
5
|
+
import {
|
|
6
|
+
analyzeStringAttributes,
|
|
7
|
+
executeMigrationPlan,
|
|
8
|
+
} from "../../migrations/migrateStrings.js";
|
|
9
|
+
import type { AnalyzeOptions, ExecuteOptions } from "../../migrations/migrateStringsTypes.js";
|
|
10
|
+
|
|
11
|
+
export const migrateCommands = {
|
|
12
|
+
async migrateStrings(cli: InteractiveCLI): Promise<void> {
|
|
13
|
+
const { phase } = await inquirer.prompt([
|
|
14
|
+
{
|
|
15
|
+
type: "list",
|
|
16
|
+
name: "phase",
|
|
17
|
+
message: "String attribute migration:",
|
|
18
|
+
choices: [
|
|
19
|
+
{
|
|
20
|
+
name: "Analyze — scan local configs, generate migration plan (YAML)",
|
|
21
|
+
value: "analyze",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "Execute — run a migration plan against Appwrite server",
|
|
25
|
+
value: "execute",
|
|
26
|
+
},
|
|
27
|
+
{ name: "Back", value: "back" },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
if (phase === "back") return;
|
|
33
|
+
if (phase === "analyze") {
|
|
34
|
+
await migrateCommands.analyzePhase(cli);
|
|
35
|
+
} else {
|
|
36
|
+
await migrateCommands.executePhase(cli);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async analyzePhase(cli: InteractiveCLI): Promise<void> {
|
|
41
|
+
const controller = (cli as any).controller;
|
|
42
|
+
if (!controller?.config) {
|
|
43
|
+
MessageFormatter.error(
|
|
44
|
+
"No configuration loaded. Make sure you have a valid .appwrite config.",
|
|
45
|
+
undefined,
|
|
46
|
+
{ prefix: "Analyze" }
|
|
47
|
+
);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const config = controller.config;
|
|
52
|
+
const collections = [
|
|
53
|
+
...(config.collections || []),
|
|
54
|
+
...(config.tables || []),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
if (collections.length === 0) {
|
|
58
|
+
MessageFormatter.warning(
|
|
59
|
+
"No collections/tables found in local config. Nothing to analyze.",
|
|
60
|
+
{ prefix: "Analyze" }
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Prompt for output path
|
|
66
|
+
const { outputPath } = await inquirer.prompt([
|
|
67
|
+
{
|
|
68
|
+
type: "input",
|
|
69
|
+
name: "outputPath",
|
|
70
|
+
message: "Output path for migration plan:",
|
|
71
|
+
default: path.join(process.cwd(), "migrate-strings-plan.yaml"),
|
|
72
|
+
},
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const options: AnalyzeOptions = { outputPath };
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
analyzeStringAttributes(config, options);
|
|
79
|
+
MessageFormatter.success(
|
|
80
|
+
"Analysis complete. Review the YAML plan, edit targetType/action as needed, then run Execute.",
|
|
81
|
+
{ prefix: "Analyze" }
|
|
82
|
+
);
|
|
83
|
+
} catch (err: any) {
|
|
84
|
+
MessageFormatter.error(
|
|
85
|
+
`Analysis failed: ${err.message}`,
|
|
86
|
+
undefined,
|
|
87
|
+
{ prefix: "Analyze" }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
async executePhase(cli: InteractiveCLI): Promise<void> {
|
|
93
|
+
const controller = (cli as any).controller;
|
|
94
|
+
if (!controller?.adapter) {
|
|
95
|
+
MessageFormatter.error(
|
|
96
|
+
"No database adapter available. Ensure a server connection is established.",
|
|
97
|
+
undefined,
|
|
98
|
+
{ prefix: "Execute" }
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Prompt for plan path
|
|
104
|
+
const { planPath } = await inquirer.prompt([
|
|
105
|
+
{
|
|
106
|
+
type: "input",
|
|
107
|
+
name: "planPath",
|
|
108
|
+
message: "Path to migration plan YAML:",
|
|
109
|
+
default: path.join(process.cwd(), "migrate-strings-plan.yaml"),
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const { keepBackups } = await inquirer.prompt([
|
|
114
|
+
{
|
|
115
|
+
type: "confirm",
|
|
116
|
+
name: "keepBackups",
|
|
117
|
+
message: "Keep backup attributes after migration? (safer, uses more attribute slots)",
|
|
118
|
+
default: true,
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const { dryRun } = await inquirer.prompt([
|
|
123
|
+
{
|
|
124
|
+
type: "confirm",
|
|
125
|
+
name: "dryRun",
|
|
126
|
+
message: "Dry run? (no actual changes)",
|
|
127
|
+
default: false,
|
|
128
|
+
},
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const options: ExecuteOptions = {
|
|
132
|
+
planPath,
|
|
133
|
+
keepBackups,
|
|
134
|
+
dryRun,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const results = await executeMigrationPlan(controller.adapter, options);
|
|
139
|
+
if (results.failed > 0) {
|
|
140
|
+
MessageFormatter.warning(
|
|
141
|
+
`Migration completed with ${results.failed} failure(s). Check checkpoint file to resume.`,
|
|
142
|
+
{ prefix: "Execute" }
|
|
143
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
MessageFormatter.success("Migration completed successfully.", {
|
|
146
|
+
prefix: "Execute",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
} catch (err: any) {
|
|
150
|
+
MessageFormatter.error(
|
|
151
|
+
`Execution failed: ${err.message}`,
|
|
152
|
+
undefined,
|
|
153
|
+
{ prefix: "Execute" }
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
};
|
package/src/interactiveCLI.ts
CHANGED
|
@@ -46,6 +46,7 @@ import { storageCommands } from "./cli/commands/storageCommands.js";
|
|
|
46
46
|
import { transferCommands } from "./cli/commands/transferCommands.js";
|
|
47
47
|
import { schemaCommands } from "./cli/commands/schemaCommands.js";
|
|
48
48
|
import { importFileCommands } from "./cli/commands/importFileCommands.js";
|
|
49
|
+
import { migrateCommands } from "./cli/commands/migrateCommands.js";
|
|
49
50
|
|
|
50
51
|
enum CHOICES {
|
|
51
52
|
MIGRATE_CONFIG = "🔄 Migrate TypeScript config to YAML (.appwrite structure)",
|
|
@@ -71,6 +72,7 @@ enum CHOICES {
|
|
|
71
72
|
RELOAD_CONFIG = "🔄 Reload configuration files",
|
|
72
73
|
UPDATE_FUNCTION_SPEC = "⚙️ Update function specifications",
|
|
73
74
|
MANAGE_BUCKETS = "🪣 Manage storage buckets",
|
|
75
|
+
MIGRATE_STRINGS = "🔄 Migrate string attributes to varchar/text types",
|
|
74
76
|
EXIT = "👋 Exit",
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -203,6 +205,10 @@ export class InteractiveCLI {
|
|
|
203
205
|
case CHOICES.MANAGE_BUCKETS:
|
|
204
206
|
await this.manageBuckets();
|
|
205
207
|
break;
|
|
208
|
+
case CHOICES.MIGRATE_STRINGS:
|
|
209
|
+
await this.initControllerIfNeeded();
|
|
210
|
+
await migrateCommands.migrateStrings(this);
|
|
211
|
+
break;
|
|
206
212
|
case CHOICES.EXIT:
|
|
207
213
|
MessageFormatter.success("Goodbye!");
|
|
208
214
|
process.exit(0);
|
package/src/main.ts
CHANGED
|
@@ -82,6 +82,12 @@ interface CliOptions {
|
|
|
82
82
|
importFile?: string;
|
|
83
83
|
targetDb?: string;
|
|
84
84
|
targetTable?: string;
|
|
85
|
+
// String attribute migration
|
|
86
|
+
migrateStringsAnalyze?: boolean;
|
|
87
|
+
migrateStringsExecute?: string;
|
|
88
|
+
migrateStringsOutput?: string;
|
|
89
|
+
migrateStringsKeepBackups?: boolean;
|
|
90
|
+
migrateStringsDryRun?: boolean;
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
type ParsedArgv = ArgumentsCamelCase<CliOptions>;
|
|
@@ -621,6 +627,32 @@ const argv = yargs(hideBin(process.argv))
|
|
|
621
627
|
type: "string",
|
|
622
628
|
description: "Target table ID for --importFile (prompted if omitted)",
|
|
623
629
|
})
|
|
630
|
+
.option("migrateStringsAnalyze", {
|
|
631
|
+
alias: ["migrate-strings-analyze"],
|
|
632
|
+
type: "boolean",
|
|
633
|
+
description: "Analyze local configs and generate a string-to-varchar/text migration plan (YAML)",
|
|
634
|
+
})
|
|
635
|
+
.option("migrateStringsExecute", {
|
|
636
|
+
alias: ["migrate-strings-execute"],
|
|
637
|
+
type: "string",
|
|
638
|
+
description: "Execute a string migration plan from the given YAML path",
|
|
639
|
+
})
|
|
640
|
+
.option("migrateStringsOutput", {
|
|
641
|
+
alias: ["migrate-strings-output"],
|
|
642
|
+
type: "string",
|
|
643
|
+
description: "Output path for the migration plan (default: ./migrate-strings-plan.yaml)",
|
|
644
|
+
})
|
|
645
|
+
.option("migrateStringsKeepBackups", {
|
|
646
|
+
alias: ["migrate-strings-keep-backups"],
|
|
647
|
+
type: "boolean",
|
|
648
|
+
default: true,
|
|
649
|
+
description: "Keep backup attributes after migration (default: true)",
|
|
650
|
+
})
|
|
651
|
+
.option("migrateStringsDryRun", {
|
|
652
|
+
alias: ["migrate-strings-dry-run"],
|
|
653
|
+
type: "boolean",
|
|
654
|
+
description: "Dry run — show what would happen without making changes",
|
|
655
|
+
})
|
|
624
656
|
.parse() as ParsedArgv;
|
|
625
657
|
|
|
626
658
|
async function main() {
|
|
@@ -842,6 +874,43 @@ async function main() {
|
|
|
842
874
|
return;
|
|
843
875
|
}
|
|
844
876
|
|
|
877
|
+
// String attribute migration (analyze or execute)
|
|
878
|
+
if (argv.migrateStringsAnalyze || argv.migrateStringsExecute) {
|
|
879
|
+
const { analyzeStringAttributes, executeMigrationPlan } = await import(
|
|
880
|
+
"./migrations/migrateStrings.js"
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
if (argv.migrateStringsAnalyze) {
|
|
884
|
+
if (!controller.config) {
|
|
885
|
+
MessageFormatter.error("No Appwrite configuration found", undefined, {
|
|
886
|
+
prefix: "Migration",
|
|
887
|
+
});
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
analyzeStringAttributes(controller.config, {
|
|
891
|
+
outputPath: argv.migrateStringsOutput,
|
|
892
|
+
});
|
|
893
|
+
} else if (argv.migrateStringsExecute) {
|
|
894
|
+
if (!controller.adapter) {
|
|
895
|
+
MessageFormatter.error(
|
|
896
|
+
"No database adapter available. Ensure config has valid credentials.",
|
|
897
|
+
undefined,
|
|
898
|
+
{ prefix: "Migration" }
|
|
899
|
+
);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
const results = await executeMigrationPlan(controller.adapter, {
|
|
903
|
+
planPath: argv.migrateStringsExecute,
|
|
904
|
+
keepBackups: argv.migrateStringsKeepBackups ?? true,
|
|
905
|
+
dryRun: argv.migrateStringsDryRun ?? false,
|
|
906
|
+
});
|
|
907
|
+
if (results.failed > 0) {
|
|
908
|
+
process.exit(1);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
|
|
845
914
|
// List backups if requested
|
|
846
915
|
if (parsedArgv.listBackups) {
|
|
847
916
|
const { AdapterFactory } = await import("@njdamstra/appwrite-utils-helpers");
|