storyblok 4.17.2 → 4.18.0-alpha.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/index.mjs +2080 -96
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -4
package/dist/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { homedir } from 'node:os';
|
|
|
7
7
|
import { loadConfig as loadConfig$1, SUPPORTED_EXTENSIONS } from 'c12';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { readPackageUp } from 'read-package-up';
|
|
10
|
-
import { Command } from 'commander';
|
|
10
|
+
import { Command, Option } from 'commander';
|
|
11
11
|
import { MultiBar, Presets } from 'cli-progress';
|
|
12
12
|
import { Spinner } from '@topcli/spinner';
|
|
13
13
|
import fs, { mkdir, writeFile, access, constants, readFile as readFile$1, appendFile, readdir, unlink } from 'node:fs/promises';
|
|
@@ -26,6 +26,7 @@ import { Octokit } from 'octokit';
|
|
|
26
26
|
import { pipeline as pipeline$1 } from 'node:stream/promises';
|
|
27
27
|
import { Buffer } from 'node:buffer';
|
|
28
28
|
import Storyblok from 'storyblok-js-client';
|
|
29
|
+
import { createTwoFilesPatch } from 'diff';
|
|
29
30
|
|
|
30
31
|
const commands = {
|
|
31
32
|
LOGIN: "login",
|
|
@@ -41,7 +42,8 @@ const commands = {
|
|
|
41
42
|
LOGS: "logs",
|
|
42
43
|
REPORTS: "reports",
|
|
43
44
|
ASSETS: "assets",
|
|
44
|
-
STORIES: "stories"
|
|
45
|
+
STORIES: "stories",
|
|
46
|
+
SCHEMA: "schema"
|
|
45
47
|
};
|
|
46
48
|
const colorPalette = {
|
|
47
49
|
PRIMARY: "#8d60ff",
|
|
@@ -61,7 +63,8 @@ const colorPalette = {
|
|
|
61
63
|
LOGS: "#4ade80",
|
|
62
64
|
REPORTS: "#4ade80",
|
|
63
65
|
ASSETS: "#f97316",
|
|
64
|
-
STORIES: "#a185ff"
|
|
66
|
+
STORIES: "#a185ff",
|
|
67
|
+
SCHEMA: "#e91e63"
|
|
65
68
|
};
|
|
66
69
|
const regions = {
|
|
67
70
|
EU: "eu",
|
|
@@ -1005,11 +1008,11 @@ function requireAuthentication(state, verbose = false) {
|
|
|
1005
1008
|
return true;
|
|
1006
1009
|
}
|
|
1007
1010
|
|
|
1008
|
-
const toCamelCase = (str) => {
|
|
1011
|
+
const toCamelCase$1 = (str) => {
|
|
1009
1012
|
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/_/g, "").replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/[^a-z0-9]([a-z])/gi, (_, letter) => letter.toUpperCase()).replace(/[^a-z0-9]/gi, "");
|
|
1010
1013
|
};
|
|
1011
1014
|
const toPascalCase = (str) => {
|
|
1012
|
-
const camelCase = toCamelCase(str);
|
|
1015
|
+
const camelCase = toCamelCase$1(str);
|
|
1013
1016
|
return camelCase ? camelCase[0].toUpperCase() + camelCase.slice(1) : camelCase;
|
|
1014
1017
|
};
|
|
1015
1018
|
const capitalize = (str) => {
|
|
@@ -1107,6 +1110,25 @@ function getPackageJson() {
|
|
|
1107
1110
|
return packageJson$1;
|
|
1108
1111
|
}
|
|
1109
1112
|
|
|
1113
|
+
async function fetchAllPages(fetchFunction, extractDataFunction) {
|
|
1114
|
+
const items = [];
|
|
1115
|
+
let page = 1;
|
|
1116
|
+
while (true) {
|
|
1117
|
+
const { data, response } = await fetchFunction(page);
|
|
1118
|
+
const totalHeader = response.headers.get("total");
|
|
1119
|
+
const fetchedItems = extractDataFunction(data);
|
|
1120
|
+
items.push(...fetchedItems);
|
|
1121
|
+
if (!totalHeader) {
|
|
1122
|
+
return items;
|
|
1123
|
+
}
|
|
1124
|
+
const total = Number(totalHeader);
|
|
1125
|
+
if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
|
|
1126
|
+
return items;
|
|
1127
|
+
}
|
|
1128
|
+
page++;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1110
1132
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
1111
1133
|
const __dirname$1 = dirname(__filename$1);
|
|
1112
1134
|
function isRegion(value) {
|
|
@@ -1195,6 +1217,10 @@ class UI {
|
|
|
1195
1217
|
this.br();
|
|
1196
1218
|
}
|
|
1197
1219
|
}
|
|
1220
|
+
/** Plain console.log passthrough — use for preformatted or multi-line text. */
|
|
1221
|
+
log(message) {
|
|
1222
|
+
this.console?.log(message);
|
|
1223
|
+
}
|
|
1198
1224
|
list(items) {
|
|
1199
1225
|
for (const item of items) {
|
|
1200
1226
|
this.console?.log(` ${item}`);
|
|
@@ -2101,14 +2127,14 @@ async function performInteractiveLogin(options) {
|
|
|
2101
2127
|
}
|
|
2102
2128
|
}
|
|
2103
2129
|
|
|
2104
|
-
const program$
|
|
2130
|
+
const program$f = getProgram();
|
|
2105
2131
|
const allRegionsText = Object.values(regions).join(",");
|
|
2106
|
-
program$
|
|
2132
|
+
program$f.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
|
|
2107
2133
|
"-r, --region <region>",
|
|
2108
2134
|
`The region you would like to work in. Please keep in mind that the region must match the region of your space. This region flag will be used for the other cli's commands. You can use the values: ${allRegionsText}.`
|
|
2109
2135
|
).action(async (options) => {
|
|
2110
2136
|
konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
|
|
2111
|
-
const verbose = program$
|
|
2137
|
+
const verbose = program$f.opts().verbose;
|
|
2112
2138
|
const { token, region } = options;
|
|
2113
2139
|
const { state, updateSession, persistCredentials } = session();
|
|
2114
2140
|
if (state.isLoggedIn && !state.envLogin) {
|
|
@@ -2166,10 +2192,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
|
|
|
2166
2192
|
konsola.br();
|
|
2167
2193
|
});
|
|
2168
2194
|
|
|
2169
|
-
const program$
|
|
2170
|
-
program$
|
|
2195
|
+
const program$e = getProgram();
|
|
2196
|
+
program$e.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
|
|
2171
2197
|
konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
|
|
2172
|
-
const verbose = program$
|
|
2198
|
+
const verbose = program$e.opts().verbose;
|
|
2173
2199
|
try {
|
|
2174
2200
|
const { state } = session();
|
|
2175
2201
|
if (!state.isLoggedIn || !state.password || !state.region) {
|
|
@@ -2216,10 +2242,10 @@ async function openSignupInBrowser(url) {
|
|
|
2216
2242
|
}
|
|
2217
2243
|
}
|
|
2218
2244
|
|
|
2219
|
-
const program$
|
|
2220
|
-
program$
|
|
2245
|
+
const program$d = getProgram();
|
|
2246
|
+
program$d.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
|
|
2221
2247
|
konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
|
|
2222
|
-
const verbose = program$
|
|
2248
|
+
const verbose = program$d.opts().verbose;
|
|
2223
2249
|
const { state } = session();
|
|
2224
2250
|
if (state.isLoggedIn && !state.envLogin) {
|
|
2225
2251
|
konsola.ok(`You are already logged in. If you want to signup with a different account, please logout first.`);
|
|
@@ -2240,10 +2266,10 @@ program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
|
|
|
2240
2266
|
konsola.br();
|
|
2241
2267
|
});
|
|
2242
2268
|
|
|
2243
|
-
const program$
|
|
2244
|
-
program$
|
|
2269
|
+
const program$c = getProgram();
|
|
2270
|
+
program$c.command(commands.USER).description("Get the current user").action(async () => {
|
|
2245
2271
|
konsola.title(`${commands.USER}`, colorPalette.USER);
|
|
2246
|
-
const verbose = program$
|
|
2272
|
+
const verbose = program$c.opts().verbose;
|
|
2247
2273
|
const { state } = session();
|
|
2248
2274
|
if (!requireAuthentication(state)) {
|
|
2249
2275
|
return;
|
|
@@ -2271,48 +2297,24 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
|
|
|
2271
2297
|
konsola.br();
|
|
2272
2298
|
});
|
|
2273
2299
|
|
|
2274
|
-
const program$
|
|
2275
|
-
const componentsCommand = program$
|
|
2300
|
+
const program$b = getProgram();
|
|
2301
|
+
const componentsCommand = program$b.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`);
|
|
2276
2302
|
|
|
2277
2303
|
const DEFAULT_COMPONENTS_FILENAME = "components";
|
|
2278
|
-
const DEFAULT_GROUPS_FILENAME = "groups";
|
|
2304
|
+
const DEFAULT_GROUPS_FILENAME$1 = "groups";
|
|
2279
2305
|
const DEFAULT_PRESETS_FILENAME = "presets";
|
|
2280
2306
|
const DEFAULT_TAGS_FILENAME = "tags";
|
|
2281
2307
|
|
|
2282
|
-
async function fetchAllPages(fetchFunction, extractDataFunction) {
|
|
2283
|
-
const items = [];
|
|
2284
|
-
let page = 1;
|
|
2285
|
-
while (true) {
|
|
2286
|
-
const { data, response } = await fetchFunction(page);
|
|
2287
|
-
const totalHeader = response.headers.get("total");
|
|
2288
|
-
const fetchedItems = extractDataFunction(data);
|
|
2289
|
-
items.push(...fetchedItems);
|
|
2290
|
-
if (!totalHeader) {
|
|
2291
|
-
return items;
|
|
2292
|
-
}
|
|
2293
|
-
const total = Number(totalHeader);
|
|
2294
|
-
if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
|
|
2295
|
-
return items;
|
|
2296
|
-
}
|
|
2297
|
-
page++;
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
2308
|
const fetchComponents = async (spaceId) => {
|
|
2302
2309
|
try {
|
|
2303
2310
|
const client = getMapiClient();
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
},
|
|
2312
|
-
throwOnError: true
|
|
2313
|
-
}),
|
|
2314
|
-
(data) => data?.components ?? []
|
|
2315
|
-
);
|
|
2311
|
+
const { data } = await client.components.list({
|
|
2312
|
+
path: {
|
|
2313
|
+
space_id: Number(spaceId)
|
|
2314
|
+
},
|
|
2315
|
+
throwOnError: true
|
|
2316
|
+
});
|
|
2317
|
+
return data?.components ?? [];
|
|
2316
2318
|
} catch (error) {
|
|
2317
2319
|
handleAPIError("pull_components", error);
|
|
2318
2320
|
}
|
|
@@ -2320,19 +2322,16 @@ const fetchComponents = async (spaceId) => {
|
|
|
2320
2322
|
const fetchComponent = async (spaceId, componentName) => {
|
|
2321
2323
|
try {
|
|
2322
2324
|
const client = getMapiClient();
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
}),
|
|
2334
|
-
(data) => data?.components ?? []
|
|
2335
|
-
);
|
|
2325
|
+
const { data } = await client.components.list({
|
|
2326
|
+
path: {
|
|
2327
|
+
space_id: Number(spaceId)
|
|
2328
|
+
},
|
|
2329
|
+
query: {
|
|
2330
|
+
search: componentName
|
|
2331
|
+
},
|
|
2332
|
+
throwOnError: true
|
|
2333
|
+
});
|
|
2334
|
+
const matches = data?.components ?? [];
|
|
2336
2335
|
return matches.find((c) => c.name === componentName);
|
|
2337
2336
|
} catch (error) {
|
|
2338
2337
|
handleAPIError("pull_components", error, `Failed to fetch component ${componentName}`);
|
|
@@ -2399,7 +2398,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2399
2398
|
const presetsFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.${suffix}.json` : `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.json`);
|
|
2400
2399
|
await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2));
|
|
2401
2400
|
}
|
|
2402
|
-
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
|
|
2401
|
+
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
|
|
2403
2402
|
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
|
|
2404
2403
|
const internalTagsFilePath = join(resolvedPath, suffix ? `${DEFAULT_TAGS_FILENAME}.${suffix}.json` : `${DEFAULT_TAGS_FILENAME}.json`);
|
|
2405
2404
|
await saveToFile(internalTagsFilePath, JSON.stringify(internalTags, null, 2));
|
|
@@ -2409,7 +2408,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2409
2408
|
const componentsFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
|
|
2410
2409
|
await saveToFile(componentsFilePath, JSON.stringify(components, null, 2));
|
|
2411
2410
|
if (groups.length > 0) {
|
|
2412
|
-
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
|
|
2411
|
+
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
|
|
2413
2412
|
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
|
|
2414
2413
|
}
|
|
2415
2414
|
if (presets.length > 0) {
|
|
@@ -2425,6 +2424,21 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2425
2424
|
}
|
|
2426
2425
|
};
|
|
2427
2426
|
|
|
2427
|
+
function isSchemaField$1(value) {
|
|
2428
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
2429
|
+
}
|
|
2430
|
+
function toWritableSchema(schema) {
|
|
2431
|
+
if (!schema) {
|
|
2432
|
+
return void 0;
|
|
2433
|
+
}
|
|
2434
|
+
const result = {};
|
|
2435
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2436
|
+
if (isSchemaField$1(value)) {
|
|
2437
|
+
result[key] = value;
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
return result;
|
|
2441
|
+
}
|
|
2428
2442
|
const pushComponent = async (space, component) => {
|
|
2429
2443
|
try {
|
|
2430
2444
|
const client = getMapiClient();
|
|
@@ -2459,10 +2473,23 @@ const updateComponent = async (space, componentId, component) => {
|
|
|
2459
2473
|
}
|
|
2460
2474
|
};
|
|
2461
2475
|
const upsertComponent = async (space, component, existingId) => {
|
|
2476
|
+
const { name, display_name, schema, is_root, is_nestable, component_group_uuid, color, icon, preview_field, internal_tag_ids } = component;
|
|
2477
|
+
const payload = {
|
|
2478
|
+
name,
|
|
2479
|
+
display_name,
|
|
2480
|
+
schema: toWritableSchema(schema),
|
|
2481
|
+
is_root,
|
|
2482
|
+
is_nestable,
|
|
2483
|
+
component_group_uuid: component_group_uuid ?? void 0,
|
|
2484
|
+
color: color ?? void 0,
|
|
2485
|
+
icon: icon ?? void 0,
|
|
2486
|
+
preview_field: preview_field ?? void 0,
|
|
2487
|
+
internal_tag_ids
|
|
2488
|
+
};
|
|
2462
2489
|
if (existingId) {
|
|
2463
|
-
return await updateComponent(space, existingId,
|
|
2490
|
+
return await updateComponent(space, existingId, payload);
|
|
2464
2491
|
} else {
|
|
2465
|
-
return await pushComponent(space,
|
|
2492
|
+
return await pushComponent(space, payload);
|
|
2466
2493
|
}
|
|
2467
2494
|
};
|
|
2468
2495
|
const pushComponentGroup = async (space, componentGroup) => {
|
|
@@ -2565,7 +2592,7 @@ const pushComponentInternalTag = async (space, componentInternalTag) => {
|
|
|
2565
2592
|
path: {
|
|
2566
2593
|
space_id: Number(space)
|
|
2567
2594
|
},
|
|
2568
|
-
body: componentInternalTag,
|
|
2595
|
+
body: { internal_tag: componentInternalTag },
|
|
2569
2596
|
throwOnError: true
|
|
2570
2597
|
});
|
|
2571
2598
|
return data.internal_tag;
|
|
@@ -2580,7 +2607,7 @@ const updateComponentInternalTag = async (space, tagId, componentInternalTag) =>
|
|
|
2580
2607
|
path: {
|
|
2581
2608
|
space_id: Number(space)
|
|
2582
2609
|
},
|
|
2583
|
-
body: componentInternalTag,
|
|
2610
|
+
body: { internal_tag: componentInternalTag },
|
|
2584
2611
|
throwOnError: true
|
|
2585
2612
|
});
|
|
2586
2613
|
return data.internal_tag;
|
|
@@ -2635,7 +2662,7 @@ async function readSeparateFiles$1(resolvedPath, suffix) {
|
|
|
2635
2662
|
});
|
|
2636
2663
|
for (const file of filteredFiles) {
|
|
2637
2664
|
const filePath = join(resolvedPath, file);
|
|
2638
|
-
if (file === `${DEFAULT_GROUPS_FILENAME}.json` || file === `${DEFAULT_GROUPS_FILENAME}.${suffix}.json`) {
|
|
2665
|
+
if (file === `${DEFAULT_GROUPS_FILENAME$1}.json` || file === `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json`) {
|
|
2639
2666
|
const result = await readJsonFile(filePath);
|
|
2640
2667
|
if (result.error) {
|
|
2641
2668
|
handleFileSystemError("read", result.error);
|
|
@@ -2687,7 +2714,7 @@ async function readConsolidatedFiles$1(resolvedPath, suffix) {
|
|
|
2687
2714
|
);
|
|
2688
2715
|
}
|
|
2689
2716
|
const [groupsResult, presetsResult, tagsResult] = await Promise.all([
|
|
2690
|
-
readJsonFile(join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`)),
|
|
2717
|
+
readJsonFile(join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`)),
|
|
2691
2718
|
readJsonFile(join(resolvedPath, suffix ? `${DEFAULT_PRESETS_FILENAME}.${suffix}.json` : `${DEFAULT_PRESETS_FILENAME}.json`)),
|
|
2692
2719
|
readJsonFile(join(resolvedPath, suffix ? `${DEFAULT_TAGS_FILENAME}.${suffix}.json` : `${DEFAULT_TAGS_FILENAME}.json`))
|
|
2693
2720
|
]);
|
|
@@ -3945,8 +3972,8 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
|
|
|
3945
3972
|
}
|
|
3946
3973
|
};
|
|
3947
3974
|
|
|
3948
|
-
const program$
|
|
3949
|
-
const languagesCommand = program$
|
|
3975
|
+
const program$a = getProgram();
|
|
3976
|
+
const languagesCommand = program$a.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`);
|
|
3950
3977
|
const pullCmd$3 = languagesCommand.command("pull").description(`Download your space's languages schema as json`).option("-f, --filename <filename>", "filename to save the file as <filename>.<suffix>.json").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. languages.<suffix>.json). By default, the space ID is used.").option("-s, --space <space>", "space ID");
|
|
3951
3978
|
pullCmd$3.action(async (options, command) => {
|
|
3952
3979
|
konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
|
|
@@ -3992,8 +4019,8 @@ pullCmd$3.action(async (options, command) => {
|
|
|
3992
4019
|
konsola.br();
|
|
3993
4020
|
});
|
|
3994
4021
|
|
|
3995
|
-
const program$
|
|
3996
|
-
const migrationsCommand = program$
|
|
4022
|
+
const program$9 = getProgram();
|
|
4023
|
+
const migrationsCommand = program$9.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`);
|
|
3997
4024
|
|
|
3998
4025
|
const getMigrationTemplate = () => {
|
|
3999
4026
|
return `export default function (block) {
|
|
@@ -5020,8 +5047,8 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
|
|
|
5020
5047
|
}
|
|
5021
5048
|
});
|
|
5022
5049
|
|
|
5023
|
-
const program$
|
|
5024
|
-
const typesCommand = program$
|
|
5050
|
+
const program$8 = getProgram();
|
|
5051
|
+
const typesCommand = program$8.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`);
|
|
5025
5052
|
|
|
5026
5053
|
const getAssetJSONSchema = (title) => ({
|
|
5027
5054
|
$id: "#/asset",
|
|
@@ -5632,7 +5659,7 @@ const getPropertyTypeAnnotation = (property, prefix, suffix) => {
|
|
|
5632
5659
|
}
|
|
5633
5660
|
};
|
|
5634
5661
|
function getStoryType(property, prefix, suffix) {
|
|
5635
|
-
return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase(property))}${suffix ?? ""}>`;
|
|
5662
|
+
return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase$1(property))}${suffix ?? ""}>`;
|
|
5636
5663
|
}
|
|
5637
5664
|
const getComponentType = (componentName, options) => {
|
|
5638
5665
|
const prefix = options.typePrefix ?? "";
|
|
@@ -6157,8 +6184,8 @@ generateCmd.action(async (options, command) => {
|
|
|
6157
6184
|
}
|
|
6158
6185
|
});
|
|
6159
6186
|
|
|
6160
|
-
const program$
|
|
6161
|
-
const datasourcesCommand = program$
|
|
6187
|
+
const program$7 = getProgram();
|
|
6188
|
+
const datasourcesCommand = program$7.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
|
|
6162
6189
|
|
|
6163
6190
|
const fetchDatasourceEntries = async (spaceId, datasourceId) => {
|
|
6164
6191
|
try {
|
|
@@ -6709,13 +6736,13 @@ async function promptForLogin(verbose) {
|
|
|
6709
6736
|
return null;
|
|
6710
6737
|
}
|
|
6711
6738
|
}
|
|
6712
|
-
const program$
|
|
6713
|
-
program$
|
|
6739
|
+
const program$6 = getProgram();
|
|
6740
|
+
program$6.command(`${commands.CREATE} [project-path]`).alias("c").description(`Scaffold a new project using Storyblok`).option("-t, --template <template>", "technology starter template").option("-b, --blueprint <blueprint>", "[DEPRECATED] use --template instead").option("--skip-space", "skip space creation").option("--token <token>", "Storyblok access token (skip space creation and use this token)").option(
|
|
6714
6741
|
"-r, --region <region>",
|
|
6715
6742
|
`The region to apply to the generated project template (does not affect space creation).`
|
|
6716
6743
|
).action(async (projectPath, options) => {
|
|
6717
6744
|
konsola.title(`${commands.CREATE}`, colorPalette.CREATE);
|
|
6718
|
-
const verbose = program$
|
|
6745
|
+
const verbose = program$6.opts().verbose;
|
|
6719
6746
|
const { template, blueprint, token } = options;
|
|
6720
6747
|
if (options.region && !isRegion(options.region)) {
|
|
6721
6748
|
handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
|
|
@@ -6934,8 +6961,8 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
6934
6961
|
konsola.br();
|
|
6935
6962
|
});
|
|
6936
6963
|
|
|
6937
|
-
const program$
|
|
6938
|
-
const logsCommand = program$
|
|
6964
|
+
const program$5 = getProgram();
|
|
6965
|
+
const logsCommand = program$5.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`);
|
|
6939
6966
|
|
|
6940
6967
|
const listCmd$1 = logsCommand.command("list").description("List logs").option("-s, --space <space>", "space ID");
|
|
6941
6968
|
listCmd$1.action(async (_options, command) => {
|
|
@@ -6960,8 +6987,8 @@ pruneCmd$1.action(async (options, command) => {
|
|
|
6960
6987
|
ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
6961
6988
|
});
|
|
6962
6989
|
|
|
6963
|
-
const program$
|
|
6964
|
-
const reportsCommand = program$
|
|
6990
|
+
const program$4 = getProgram();
|
|
6991
|
+
const reportsCommand = program$4.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.");
|
|
6965
6992
|
|
|
6966
6993
|
const listCmd = reportsCommand.command("list").description("List reports").option("-s, --space <space>", "space ID");
|
|
6967
6994
|
listCmd.action(async (_options, command) => {
|
|
@@ -6986,8 +7013,8 @@ pruneCmd.action(async (options, command) => {
|
|
|
6986
7013
|
ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
6987
7014
|
});
|
|
6988
7015
|
|
|
6989
|
-
const program$
|
|
6990
|
-
const assetsCommand = program$
|
|
7016
|
+
const program$3 = getProgram();
|
|
7017
|
+
const assetsCommand = program$3.command(commands.ASSETS).description(`Manage your space's assets`);
|
|
6991
7018
|
|
|
6992
7019
|
const fetchAssets = async ({ spaceId, params }) => {
|
|
6993
7020
|
try {
|
|
@@ -7177,7 +7204,7 @@ const isRemoteSource = (assetBinaryPath) => {
|
|
|
7177
7204
|
return false;
|
|
7178
7205
|
}
|
|
7179
7206
|
};
|
|
7180
|
-
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.
|
|
7207
|
+
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.new_filename);
|
|
7181
7208
|
const loadAssetMap = async (manifestFile) => {
|
|
7182
7209
|
const manifest = await loadManifest(manifestFile);
|
|
7183
7210
|
const entries = manifest.filter(isValidManifestEntry).map((e) => [
|
|
@@ -7878,8 +7905,8 @@ const traverseAndMapBySchema = (data, {
|
|
|
7878
7905
|
const dataNew = { ...data };
|
|
7879
7906
|
for (const [fieldName, fieldValue] of Object.entries(data)) {
|
|
7880
7907
|
const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
|
|
7881
|
-
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema
|
|
7882
|
-
const fieldRefMapper = typeof fieldType === "string"
|
|
7908
|
+
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema ? fieldSchema.type : void 0;
|
|
7909
|
+
const fieldRefMapper = typeof fieldType === "string" ? fieldRefMappers2[fieldType] : void 0;
|
|
7883
7910
|
if (fieldRefMapper) {
|
|
7884
7911
|
dataNew[fieldName] = fieldRefMapper(fieldValue, {
|
|
7885
7912
|
schema: fieldSchema,
|
|
@@ -7944,6 +7971,9 @@ const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRef
|
|
|
7944
7971
|
fieldRefMappers: fieldRefMappers2
|
|
7945
7972
|
});
|
|
7946
7973
|
const multilinkFieldRefMapper = (data, { maps }) => {
|
|
7974
|
+
if (!data || typeof data !== "object") {
|
|
7975
|
+
return data;
|
|
7976
|
+
}
|
|
7947
7977
|
if (data.linktype !== "story") {
|
|
7948
7978
|
return data;
|
|
7949
7979
|
}
|
|
@@ -7963,6 +7993,9 @@ const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMap
|
|
|
7963
7993
|
}));
|
|
7964
7994
|
};
|
|
7965
7995
|
const assetFieldRefMapper = (data, { maps }) => {
|
|
7996
|
+
if (!data || typeof data !== "object") {
|
|
7997
|
+
return data;
|
|
7998
|
+
}
|
|
7966
7999
|
const mappedAsset = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
|
|
7967
8000
|
if (!mappedAsset) {
|
|
7968
8001
|
return data;
|
|
@@ -7980,7 +8013,7 @@ const multiassetFieldRefMapper = (data, options) => {
|
|
|
7980
8013
|
return data.map((d) => assetFieldRefMapper(d, options));
|
|
7981
8014
|
};
|
|
7982
8015
|
const optionsFieldRefMapper = (data, { schema, maps }) => {
|
|
7983
|
-
if (schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
8016
|
+
if (!schema || !("source" in schema) || schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
7984
8017
|
return data;
|
|
7985
8018
|
}
|
|
7986
8019
|
return data.map((d) => maps.stories?.get(d) || d);
|
|
@@ -8897,8 +8930,8 @@ pushCmd$1.action(async (assetInput, options, command) => {
|
|
|
8897
8930
|
}
|
|
8898
8931
|
});
|
|
8899
8932
|
|
|
8900
|
-
const program$
|
|
8901
|
-
const storiesCommand = program$
|
|
8933
|
+
const program$2 = getProgram();
|
|
8934
|
+
const storiesCommand = program$2.command(commands.STORIES).description(`Manage your space's stories`);
|
|
8902
8935
|
|
|
8903
8936
|
const pullCmd = storiesCommand.command("pull").option("-s, --space <space>", "space ID").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-q, --query <query>", 'Filter stories by content attributes using Storyblok filter query syntax. Example: --query="[highlighted][in]=true"').option("--starts-with <path>", 'Filter stories by path. Example: --starts-with="/en/blog/"').description(`Download your space's stories as separate json files.`);
|
|
8904
8937
|
pullCmd.action(async (options, command) => {
|
|
@@ -9391,6 +9424,1957 @@ pushCmd.action(async (options, command) => {
|
|
|
9391
9424
|
}
|
|
9392
9425
|
});
|
|
9393
9426
|
|
|
9427
|
+
const program$1 = getProgram();
|
|
9428
|
+
const schemaCommand = program$1.command(commands.SCHEMA).description(`Manage your space's schema from code`);
|
|
9429
|
+
|
|
9430
|
+
function isObject(value) {
|
|
9431
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9432
|
+
}
|
|
9433
|
+
function isComponent(value) {
|
|
9434
|
+
return isObject(value) && typeof value.name === "string" && "schema" in value && isObject(value.schema);
|
|
9435
|
+
}
|
|
9436
|
+
function isDatasource(value) {
|
|
9437
|
+
return isObject(value) && typeof value.name === "string" && typeof value.slug === "string" && !("schema" in value);
|
|
9438
|
+
}
|
|
9439
|
+
function isComponentFolder(value) {
|
|
9440
|
+
return isObject(value) && typeof value.name === "string" && !("schema" in value) && !("slug" in value) && ("uuid" in value || "parent_id" in value);
|
|
9441
|
+
}
|
|
9442
|
+
function isSchemaObject(value) {
|
|
9443
|
+
return isObject(value) && ("blocks" in value || "blockFolders" in value || "datasources" in value);
|
|
9444
|
+
}
|
|
9445
|
+
function classifyExports(moduleExports) {
|
|
9446
|
+
const components = [];
|
|
9447
|
+
const componentFolders = [];
|
|
9448
|
+
const datasources = [];
|
|
9449
|
+
function collect(value) {
|
|
9450
|
+
if (isComponent(value)) {
|
|
9451
|
+
components.push(value);
|
|
9452
|
+
} else if (isDatasource(value)) {
|
|
9453
|
+
datasources.push(value);
|
|
9454
|
+
} else if (isComponentFolder(value)) {
|
|
9455
|
+
componentFolders.push(value);
|
|
9456
|
+
}
|
|
9457
|
+
}
|
|
9458
|
+
for (const value of Object.values(moduleExports)) {
|
|
9459
|
+
if (isSchemaObject(value)) {
|
|
9460
|
+
for (const group of Object.values(value)) {
|
|
9461
|
+
if (isObject(group)) {
|
|
9462
|
+
for (const entity of Object.values(group)) {
|
|
9463
|
+
collect(entity);
|
|
9464
|
+
}
|
|
9465
|
+
}
|
|
9466
|
+
}
|
|
9467
|
+
} else {
|
|
9468
|
+
collect(value);
|
|
9469
|
+
}
|
|
9470
|
+
}
|
|
9471
|
+
return { components, componentFolders, datasources };
|
|
9472
|
+
}
|
|
9473
|
+
async function loadSchema(entryPath) {
|
|
9474
|
+
const { createJiti } = await import('jiti');
|
|
9475
|
+
const jiti = createJiti(import.meta.url, {
|
|
9476
|
+
interopDefault: true
|
|
9477
|
+
});
|
|
9478
|
+
const absolutePath = (await import('pathe')).resolve(entryPath);
|
|
9479
|
+
const mod = await jiti.import(absolutePath);
|
|
9480
|
+
return classifyExports(mod);
|
|
9481
|
+
}
|
|
9482
|
+
|
|
9483
|
+
const COMPONENT_STRIP_KEYS = /* @__PURE__ */ new Set([
|
|
9484
|
+
"id",
|
|
9485
|
+
"created_at",
|
|
9486
|
+
"updated_at",
|
|
9487
|
+
"real_name",
|
|
9488
|
+
// API-computed display/technical name, read-only
|
|
9489
|
+
"preset_id",
|
|
9490
|
+
// Instance-level preset selection, not part of schema definition
|
|
9491
|
+
"all_presets",
|
|
9492
|
+
// Computed list of presets, managed via /presets API
|
|
9493
|
+
"internal_tags_list",
|
|
9494
|
+
// Read-only expanded form of internal_tag_ids ({id, name} objects)
|
|
9495
|
+
"content_type_asset_preview",
|
|
9496
|
+
// Read-only, not in ComponentCreate/ComponentUpdate
|
|
9497
|
+
"image",
|
|
9498
|
+
// Read-only preview image URL
|
|
9499
|
+
"preview_tmpl",
|
|
9500
|
+
// Read-only preview template
|
|
9501
|
+
"metadata"
|
|
9502
|
+
// Not in current API types, stripped defensively
|
|
9503
|
+
]);
|
|
9504
|
+
const DATASOURCE_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
|
|
9505
|
+
const DATASOURCE_DIMENSION_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "datasource_id", "created_at", "updated_at"]);
|
|
9506
|
+
const FOLDER_INIT_STRIP_KEYS = /* @__PURE__ */ new Set(["id"]);
|
|
9507
|
+
const FOLDER_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "uuid"]);
|
|
9508
|
+
const DATASOURCE_DEFAULTS = {
|
|
9509
|
+
dimensions: []
|
|
9510
|
+
};
|
|
9511
|
+
const COMPONENT_DEFAULTS = {
|
|
9512
|
+
display_name: "",
|
|
9513
|
+
color: "",
|
|
9514
|
+
icon: "",
|
|
9515
|
+
preview_field: "",
|
|
9516
|
+
internal_tag_ids: []
|
|
9517
|
+
};
|
|
9518
|
+
function applyDefaults(entity, defaults) {
|
|
9519
|
+
const result = { ...entity };
|
|
9520
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
9521
|
+
if (result[key] === void 0 || result[key] === null) {
|
|
9522
|
+
Object.assign(result, { [key]: defaultValue });
|
|
9523
|
+
}
|
|
9524
|
+
}
|
|
9525
|
+
return result;
|
|
9526
|
+
}
|
|
9527
|
+
const INDENT = " ";
|
|
9528
|
+
function formatValue(value, depth) {
|
|
9529
|
+
const indent = INDENT.repeat(depth);
|
|
9530
|
+
const innerIndent = INDENT.repeat(depth + 1);
|
|
9531
|
+
if (value === null || value === void 0) {
|
|
9532
|
+
return String(value);
|
|
9533
|
+
}
|
|
9534
|
+
if (typeof value === "string") {
|
|
9535
|
+
return `'${value.replace(/'/g, "\\'")}'`;
|
|
9536
|
+
}
|
|
9537
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
9538
|
+
return String(value);
|
|
9539
|
+
}
|
|
9540
|
+
if (Array.isArray(value)) {
|
|
9541
|
+
if (value.length === 0) {
|
|
9542
|
+
return "[]";
|
|
9543
|
+
}
|
|
9544
|
+
const items = value.map((item) => `${innerIndent}${formatValue(item, depth + 1)},`);
|
|
9545
|
+
return `[
|
|
9546
|
+
${items.join("\n")}
|
|
9547
|
+
${indent}]`;
|
|
9548
|
+
}
|
|
9549
|
+
if (typeof value === "object") {
|
|
9550
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0 && v !== null).sort(([a], [b]) => a.localeCompare(b));
|
|
9551
|
+
if (entries.length === 0) {
|
|
9552
|
+
return "{}";
|
|
9553
|
+
}
|
|
9554
|
+
const props = entries.map(
|
|
9555
|
+
([key, val]) => `${innerIndent}${key}: ${formatValue(val, depth + 1)},`
|
|
9556
|
+
);
|
|
9557
|
+
return `{
|
|
9558
|
+
${props.join("\n")}
|
|
9559
|
+
${indent}}`;
|
|
9560
|
+
}
|
|
9561
|
+
return String(value);
|
|
9562
|
+
}
|
|
9563
|
+
function fileTimestamp(iso) {
|
|
9564
|
+
return iso.replace(/[:.]/g, "-");
|
|
9565
|
+
}
|
|
9566
|
+
function stripKeys(obj, keysToStrip) {
|
|
9567
|
+
const result = {};
|
|
9568
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
9569
|
+
if (!keysToStrip.has(key) && value !== void 0 && value !== null) {
|
|
9570
|
+
result[key] = value;
|
|
9571
|
+
}
|
|
9572
|
+
}
|
|
9573
|
+
return result;
|
|
9574
|
+
}
|
|
9575
|
+
|
|
9576
|
+
function sortSchemaByPos$1(schema) {
|
|
9577
|
+
const entries = Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
|
|
9578
|
+
const posA = typeof a.pos === "number" ? a.pos : Infinity;
|
|
9579
|
+
const posB = typeof b.pos === "number" ? b.pos : Infinity;
|
|
9580
|
+
return posA - posB;
|
|
9581
|
+
});
|
|
9582
|
+
return Object.fromEntries(
|
|
9583
|
+
entries.map(([key, field]) => {
|
|
9584
|
+
const { id, ...rest } = field;
|
|
9585
|
+
return [key, rest];
|
|
9586
|
+
})
|
|
9587
|
+
);
|
|
9588
|
+
}
|
|
9589
|
+
function serializeComponent(component) {
|
|
9590
|
+
const clean = stripKeys(component, COMPONENT_STRIP_KEYS);
|
|
9591
|
+
if (clean.schema && typeof clean.schema === "object") {
|
|
9592
|
+
clean.schema = sortSchemaByPos$1(clean.schema);
|
|
9593
|
+
}
|
|
9594
|
+
const ordered = {};
|
|
9595
|
+
if (clean.name !== void 0) {
|
|
9596
|
+
ordered.name = clean.name;
|
|
9597
|
+
}
|
|
9598
|
+
if (clean.display_name !== void 0) {
|
|
9599
|
+
ordered.display_name = clean.display_name;
|
|
9600
|
+
}
|
|
9601
|
+
if (clean.is_root !== void 0) {
|
|
9602
|
+
ordered.is_root = clean.is_root;
|
|
9603
|
+
}
|
|
9604
|
+
if (clean.is_nestable !== void 0) {
|
|
9605
|
+
ordered.is_nestable = clean.is_nestable;
|
|
9606
|
+
}
|
|
9607
|
+
const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
|
|
9608
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
9609
|
+
if (!handled.has(key)) {
|
|
9610
|
+
ordered[key] = value;
|
|
9611
|
+
}
|
|
9612
|
+
}
|
|
9613
|
+
if (clean.schema !== void 0) {
|
|
9614
|
+
ordered.schema = clean.schema;
|
|
9615
|
+
}
|
|
9616
|
+
return `defineBlock(${formatValue(ordered, 0)})`;
|
|
9617
|
+
}
|
|
9618
|
+
function serializeDatasource(datasource) {
|
|
9619
|
+
const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
|
|
9620
|
+
if (Array.isArray(clean.dimensions)) {
|
|
9621
|
+
clean.dimensions = clean.dimensions.map(
|
|
9622
|
+
(dim) => typeof dim === "object" && dim !== null ? stripKeys(dim, DATASOURCE_DIMENSION_STRIP_KEYS) : dim
|
|
9623
|
+
);
|
|
9624
|
+
}
|
|
9625
|
+
const ordered = {};
|
|
9626
|
+
if (clean.name !== void 0) {
|
|
9627
|
+
ordered.name = clean.name;
|
|
9628
|
+
}
|
|
9629
|
+
if (clean.slug !== void 0) {
|
|
9630
|
+
ordered.slug = clean.slug;
|
|
9631
|
+
}
|
|
9632
|
+
const handled = /* @__PURE__ */ new Set(["name", "slug"]);
|
|
9633
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
9634
|
+
if (!handled.has(key)) {
|
|
9635
|
+
ordered[key] = value;
|
|
9636
|
+
}
|
|
9637
|
+
}
|
|
9638
|
+
return `defineDatasource(${formatValue(ordered, 0)})`;
|
|
9639
|
+
}
|
|
9640
|
+
function serializeComponentFolder(folder) {
|
|
9641
|
+
const clean = stripKeys(folder, FOLDER_STRIP_KEYS);
|
|
9642
|
+
const ordered = {};
|
|
9643
|
+
if (clean.name !== void 0) {
|
|
9644
|
+
ordered.name = clean.name;
|
|
9645
|
+
}
|
|
9646
|
+
const handled = /* @__PURE__ */ new Set(["name"]);
|
|
9647
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
9648
|
+
if (!handled.has(key)) {
|
|
9649
|
+
ordered[key] = value;
|
|
9650
|
+
}
|
|
9651
|
+
}
|
|
9652
|
+
return `defineBlockFolder(${formatValue(ordered, 0)})`;
|
|
9653
|
+
}
|
|
9654
|
+
|
|
9655
|
+
function diffEntity(type, name, localSerialized, remoteSerialized) {
|
|
9656
|
+
if (!remoteSerialized && localSerialized) {
|
|
9657
|
+
return { type, name, action: "create", diff: null, local: null, remote: null };
|
|
9658
|
+
}
|
|
9659
|
+
if (remoteSerialized && !localSerialized) {
|
|
9660
|
+
return { type, name, action: "stale", diff: null, local: null, remote: null };
|
|
9661
|
+
}
|
|
9662
|
+
if (localSerialized === remoteSerialized) {
|
|
9663
|
+
return { type, name, action: "unchanged", diff: null, local: null, remote: null };
|
|
9664
|
+
}
|
|
9665
|
+
const patch = createTwoFilesPatch(
|
|
9666
|
+
`remote/${name}`,
|
|
9667
|
+
`local/${name}`,
|
|
9668
|
+
remoteSerialized,
|
|
9669
|
+
localSerialized,
|
|
9670
|
+
"remote",
|
|
9671
|
+
"local"
|
|
9672
|
+
);
|
|
9673
|
+
return { type, name, action: "update", diff: patch, local: null, remote: null };
|
|
9674
|
+
}
|
|
9675
|
+
function diffSchema(local, remote) {
|
|
9676
|
+
const diffs = [];
|
|
9677
|
+
const processedComponentNames = /* @__PURE__ */ new Set();
|
|
9678
|
+
for (const comp of local.components) {
|
|
9679
|
+
processedComponentNames.add(comp.name);
|
|
9680
|
+
const remoteComp = remote.components.get(comp.name);
|
|
9681
|
+
const localSerialized = serializeComponent(applyDefaults(comp, COMPONENT_DEFAULTS));
|
|
9682
|
+
const remoteSerialized = remoteComp ? serializeComponent(applyDefaults(remoteComp, COMPONENT_DEFAULTS)) : null;
|
|
9683
|
+
diffs.push(diffEntity("component", comp.name, localSerialized, remoteSerialized));
|
|
9684
|
+
}
|
|
9685
|
+
for (const [name] of remote.components) {
|
|
9686
|
+
if (!processedComponentNames.has(name)) {
|
|
9687
|
+
diffs.push(diffEntity("component", name, null, "stale"));
|
|
9688
|
+
}
|
|
9689
|
+
}
|
|
9690
|
+
const processedFolderNames = /* @__PURE__ */ new Set();
|
|
9691
|
+
for (const folder of local.componentFolders) {
|
|
9692
|
+
processedFolderNames.add(folder.name);
|
|
9693
|
+
const remoteFolder = remote.componentFolders.get(folder.name);
|
|
9694
|
+
const localSerialized = serializeComponentFolder(folder);
|
|
9695
|
+
const remoteSerialized = remoteFolder ? serializeComponentFolder(remoteFolder) : null;
|
|
9696
|
+
diffs.push(diffEntity("componentFolder", folder.name, localSerialized, remoteSerialized));
|
|
9697
|
+
}
|
|
9698
|
+
for (const [name] of remote.componentFolders) {
|
|
9699
|
+
if (!processedFolderNames.has(name)) {
|
|
9700
|
+
diffs.push(diffEntity("componentFolder", name, null, "stale"));
|
|
9701
|
+
}
|
|
9702
|
+
}
|
|
9703
|
+
const processedDatasourceNames = /* @__PURE__ */ new Set();
|
|
9704
|
+
for (const ds of local.datasources) {
|
|
9705
|
+
processedDatasourceNames.add(ds.name);
|
|
9706
|
+
const remoteDs = remote.datasources.get(ds.name);
|
|
9707
|
+
const localSerialized = serializeDatasource(applyDefaults(ds, DATASOURCE_DEFAULTS));
|
|
9708
|
+
const remoteSerialized = remoteDs ? serializeDatasource(applyDefaults(remoteDs, DATASOURCE_DEFAULTS)) : null;
|
|
9709
|
+
diffs.push(diffEntity("datasource", ds.name, localSerialized, remoteSerialized));
|
|
9710
|
+
}
|
|
9711
|
+
for (const [name] of remote.datasources) {
|
|
9712
|
+
if (!processedDatasourceNames.has(name)) {
|
|
9713
|
+
diffs.push(diffEntity("datasource", name, null, "stale"));
|
|
9714
|
+
}
|
|
9715
|
+
}
|
|
9716
|
+
return {
|
|
9717
|
+
diffs,
|
|
9718
|
+
creates: diffs.filter((d) => d.action === "create").length,
|
|
9719
|
+
updates: diffs.filter((d) => d.action === "update").length,
|
|
9720
|
+
unchanged: diffs.filter((d) => d.action === "unchanged").length,
|
|
9721
|
+
stale: diffs.filter((d) => d.action === "stale").length
|
|
9722
|
+
};
|
|
9723
|
+
}
|
|
9724
|
+
|
|
9725
|
+
async function fetchRemoteSchema(spaceId) {
|
|
9726
|
+
const client = getMapiClient();
|
|
9727
|
+
const spaceIdNum = Number(spaceId);
|
|
9728
|
+
const [componentsRes, foldersRes, rawDatasources] = await Promise.all([
|
|
9729
|
+
client.components.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
|
|
9730
|
+
client.componentFolders.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
|
|
9731
|
+
fetchAllPages(
|
|
9732
|
+
(page) => client.datasources.list({ path: { space_id: spaceIdNum }, query: { page }, throwOnError: true }),
|
|
9733
|
+
(data) => data?.datasources ?? []
|
|
9734
|
+
)
|
|
9735
|
+
]);
|
|
9736
|
+
const rawComponents = componentsRes.data?.components ?? [];
|
|
9737
|
+
const rawComponentFolders = foldersRes.data?.component_groups ?? [];
|
|
9738
|
+
const remote = {
|
|
9739
|
+
components: new Map(rawComponents.map((c) => [c.name, c])),
|
|
9740
|
+
componentFolders: new Map(rawComponentFolders.map((f) => [f.name, f])),
|
|
9741
|
+
datasources: new Map(rawDatasources.map((d) => [d.name, d]))
|
|
9742
|
+
};
|
|
9743
|
+
return { remote, rawComponents, rawComponentFolders, rawDatasources };
|
|
9744
|
+
}
|
|
9745
|
+
|
|
9746
|
+
function isRecord(value) {
|
|
9747
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9748
|
+
}
|
|
9749
|
+
function isSchemaField(value) {
|
|
9750
|
+
return isRecord(value) && "type" in value;
|
|
9751
|
+
}
|
|
9752
|
+
function toSchemaRecord(schema) {
|
|
9753
|
+
const result = {};
|
|
9754
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
9755
|
+
if (key === "_uid" || key === "component" || !isSchemaField(value)) {
|
|
9756
|
+
continue;
|
|
9757
|
+
}
|
|
9758
|
+
result[key] = value;
|
|
9759
|
+
}
|
|
9760
|
+
return result;
|
|
9761
|
+
}
|
|
9762
|
+
function buildComponentPayload(input) {
|
|
9763
|
+
if (!isRecord(input)) {
|
|
9764
|
+
return { name: "" };
|
|
9765
|
+
}
|
|
9766
|
+
return {
|
|
9767
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
9768
|
+
// Fields in COMPONENT_DEFAULTS are always sent with their reset value so that
|
|
9769
|
+
// removing a field from the local schema actually clears it on the API.
|
|
9770
|
+
// (Root-level fields are additive on MAPI update — omitting preserves the old value.)
|
|
9771
|
+
display_name: typeof input.display_name === "string" ? input.display_name : "",
|
|
9772
|
+
color: typeof input.color === "string" ? input.color : "",
|
|
9773
|
+
icon: typeof input.icon === "string" ? input.icon : "",
|
|
9774
|
+
preview_field: typeof input.preview_field === "string" ? input.preview_field : "",
|
|
9775
|
+
internal_tag_ids: Array.isArray(input.internal_tag_ids) ? input.internal_tag_ids : [],
|
|
9776
|
+
// Conditionally sent: only included when explicitly set in local schema
|
|
9777
|
+
...isRecord(input.schema) && { schema: toSchemaRecord(input.schema) },
|
|
9778
|
+
...typeof input.is_root === "boolean" && { is_root: input.is_root },
|
|
9779
|
+
...typeof input.is_nestable === "boolean" && { is_nestable: input.is_nestable },
|
|
9780
|
+
...typeof input.component_group_uuid === "string" && { component_group_uuid: input.component_group_uuid }
|
|
9781
|
+
};
|
|
9782
|
+
}
|
|
9783
|
+
function toComponentCreate(input) {
|
|
9784
|
+
return buildComponentPayload(input);
|
|
9785
|
+
}
|
|
9786
|
+
function toComponentUpdate(input) {
|
|
9787
|
+
return buildComponentPayload(input);
|
|
9788
|
+
}
|
|
9789
|
+
function toComponentFolderCreate(input) {
|
|
9790
|
+
if (!isRecord(input)) {
|
|
9791
|
+
return { name: "" };
|
|
9792
|
+
}
|
|
9793
|
+
return {
|
|
9794
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
9795
|
+
...typeof input.parent_id === "number" && { parent_id: input.parent_id }
|
|
9796
|
+
};
|
|
9797
|
+
}
|
|
9798
|
+
function toDatasourceCreate(input) {
|
|
9799
|
+
if (!isRecord(input)) {
|
|
9800
|
+
return { name: "", slug: "" };
|
|
9801
|
+
}
|
|
9802
|
+
const result = {
|
|
9803
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
9804
|
+
slug: typeof input.slug === "string" ? input.slug : ""
|
|
9805
|
+
};
|
|
9806
|
+
if (Array.isArray(input.dimensions)) {
|
|
9807
|
+
result.dimensions_attributes = input.dimensions.filter((d) => isRecord(d) && typeof d.name === "string" && typeof d.entry_value === "string").map((d) => ({
|
|
9808
|
+
name: d.name,
|
|
9809
|
+
entry_value: d.entry_value
|
|
9810
|
+
}));
|
|
9811
|
+
}
|
|
9812
|
+
return result;
|
|
9813
|
+
}
|
|
9814
|
+
function toDatasourceUpdate(input, remote) {
|
|
9815
|
+
const base = toDatasourceCreate(input);
|
|
9816
|
+
const localDims = base.dimensions_attributes ?? [];
|
|
9817
|
+
const remoteDims = remote.dimensions ?? [];
|
|
9818
|
+
if (remoteDims.length === 0) {
|
|
9819
|
+
return base;
|
|
9820
|
+
}
|
|
9821
|
+
const localKeys = new Set(localDims.map((d) => `${d.name}::${d.entry_value}`));
|
|
9822
|
+
const destroyEntries = remoteDims.filter((rd) => rd.id != null && !localKeys.has(`${rd.name}::${rd.entry_value}`)).map((rd) => ({ id: rd.id, _destroy: true }));
|
|
9823
|
+
if (destroyEntries.length > 0) {
|
|
9824
|
+
return {
|
|
9825
|
+
...base,
|
|
9826
|
+
dimensions_attributes: [...localDims, ...destroyEntries]
|
|
9827
|
+
};
|
|
9828
|
+
}
|
|
9829
|
+
return base;
|
|
9830
|
+
}
|
|
9831
|
+
|
|
9832
|
+
function formatDiffOutput(result, options) {
|
|
9833
|
+
const lines = [];
|
|
9834
|
+
const byType = {
|
|
9835
|
+
component: [],
|
|
9836
|
+
componentFolder: [],
|
|
9837
|
+
datasource: []
|
|
9838
|
+
};
|
|
9839
|
+
for (const diff of result.diffs) {
|
|
9840
|
+
byType[diff.type].push(diff);
|
|
9841
|
+
}
|
|
9842
|
+
const willDelete = options?.delete ?? false;
|
|
9843
|
+
const icons = {
|
|
9844
|
+
create: chalk.green("+"),
|
|
9845
|
+
update: chalk.yellow("~"),
|
|
9846
|
+
unchanged: chalk.dim("="),
|
|
9847
|
+
stale: chalk.red("-")
|
|
9848
|
+
};
|
|
9849
|
+
const sections = [
|
|
9850
|
+
["Components", byType.component],
|
|
9851
|
+
["Component Folders", byType.componentFolder],
|
|
9852
|
+
["Datasources", byType.datasource]
|
|
9853
|
+
];
|
|
9854
|
+
for (const [label, diffs] of sections) {
|
|
9855
|
+
if (diffs.length === 0) {
|
|
9856
|
+
continue;
|
|
9857
|
+
}
|
|
9858
|
+
lines.push(chalk.bold(label));
|
|
9859
|
+
for (const diff of diffs) {
|
|
9860
|
+
const icon = icons[diff.action] ?? " ";
|
|
9861
|
+
const name = diff.action === "stale" ? chalk.red(diff.name) : diff.name;
|
|
9862
|
+
const actionLabel = diff.action === "stale" && willDelete ? "delete" : diff.action;
|
|
9863
|
+
lines.push(` ${icon} ${name} ${chalk.dim(`(${actionLabel})`)}`);
|
|
9864
|
+
if (diff.diff) {
|
|
9865
|
+
for (const line of diff.diff.split("\n")) {
|
|
9866
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
9867
|
+
lines.push(` ${chalk.green(line)}`);
|
|
9868
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
9869
|
+
lines.push(` ${chalk.red(line)}`);
|
|
9870
|
+
}
|
|
9871
|
+
}
|
|
9872
|
+
}
|
|
9873
|
+
}
|
|
9874
|
+
lines.push("");
|
|
9875
|
+
}
|
|
9876
|
+
const summary = [
|
|
9877
|
+
result.creates > 0 ? chalk.green(`${result.creates} to create`) : null,
|
|
9878
|
+
result.updates > 0 ? chalk.yellow(`${result.updates} to update`) : null,
|
|
9879
|
+
result.unchanged > 0 ? chalk.dim(`${result.unchanged} unchanged`) : null,
|
|
9880
|
+
result.stale > 0 ? chalk.red(`${result.stale} ${willDelete ? "to delete" : "stale"}`) : null
|
|
9881
|
+
].filter(Boolean).join(", ");
|
|
9882
|
+
lines.push(`Summary: ${summary}`);
|
|
9883
|
+
return lines.join("\n");
|
|
9884
|
+
}
|
|
9885
|
+
async function executePush(spaceId, local, remote, diffResult, options) {
|
|
9886
|
+
const client = getMapiClient();
|
|
9887
|
+
const spaceIdNum = Number(spaceId);
|
|
9888
|
+
let created = 0;
|
|
9889
|
+
let updated = 0;
|
|
9890
|
+
let deleted = 0;
|
|
9891
|
+
const createdFolderUuids = /* @__PURE__ */ new Map();
|
|
9892
|
+
const folderDiffs = diffResult.diffs.filter((d) => d.type === "componentFolder");
|
|
9893
|
+
const folderResults = await Promise.allSettled(
|
|
9894
|
+
folderDiffs.map(async (diff) => {
|
|
9895
|
+
const localFolder = local.componentFolders.find((f) => f.name === diff.name);
|
|
9896
|
+
if (diff.action === "create" && localFolder) {
|
|
9897
|
+
const response = await client.componentFolders.create({
|
|
9898
|
+
path: { space_id: spaceIdNum },
|
|
9899
|
+
body: { component_group: toComponentFolderCreate(localFolder) },
|
|
9900
|
+
throwOnError: true
|
|
9901
|
+
});
|
|
9902
|
+
return { action: "created", uuid: response.data?.component_group?.uuid };
|
|
9903
|
+
}
|
|
9904
|
+
if (diff.action === "update" && localFolder) {
|
|
9905
|
+
const existing = remote.componentFolders.get(diff.name);
|
|
9906
|
+
if (existing?.id) {
|
|
9907
|
+
await client.componentFolders.update(existing.id, {
|
|
9908
|
+
path: { space_id: spaceIdNum },
|
|
9909
|
+
body: { component_group: toComponentFolderCreate(localFolder) },
|
|
9910
|
+
throwOnError: true
|
|
9911
|
+
});
|
|
9912
|
+
return { action: "updated" };
|
|
9913
|
+
}
|
|
9914
|
+
}
|
|
9915
|
+
})
|
|
9916
|
+
);
|
|
9917
|
+
for (let i = 0; i < folderResults.length; i++) {
|
|
9918
|
+
const result = folderResults[i];
|
|
9919
|
+
const diff = folderDiffs[i];
|
|
9920
|
+
if (result.status === "fulfilled") {
|
|
9921
|
+
if (result.value?.action === "created") {
|
|
9922
|
+
if (result.value.uuid) {
|
|
9923
|
+
createdFolderUuids.set(diff.name, result.value.uuid);
|
|
9924
|
+
}
|
|
9925
|
+
created++;
|
|
9926
|
+
} else if (result.value?.action === "updated") {
|
|
9927
|
+
updated++;
|
|
9928
|
+
}
|
|
9929
|
+
} else {
|
|
9930
|
+
const eventId = diff.action === "create" ? "push_component_group" : "update_component_group";
|
|
9931
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} component folder ${diff.name}`);
|
|
9932
|
+
}
|
|
9933
|
+
}
|
|
9934
|
+
const skippedComponents = /* @__PURE__ */ new Set();
|
|
9935
|
+
if (options.pendingFolderAssignments) {
|
|
9936
|
+
const logger = getLogger();
|
|
9937
|
+
for (const [folderName, componentNames] of options.pendingFolderAssignments) {
|
|
9938
|
+
const remoteUuid = createdFolderUuids.get(folderName);
|
|
9939
|
+
if (!remoteUuid) {
|
|
9940
|
+
logger.warn(`Could not resolve folder '${folderName}' \u2014 skipping components: ${componentNames.join(", ")}`);
|
|
9941
|
+
for (const name of componentNames) {
|
|
9942
|
+
skippedComponents.add(name);
|
|
9943
|
+
}
|
|
9944
|
+
continue;
|
|
9945
|
+
}
|
|
9946
|
+
for (const compName of componentNames) {
|
|
9947
|
+
const comp = local.components.find((c) => c.name === compName);
|
|
9948
|
+
if (comp) {
|
|
9949
|
+
comp.component_group_uuid = remoteUuid;
|
|
9950
|
+
}
|
|
9951
|
+
}
|
|
9952
|
+
}
|
|
9953
|
+
}
|
|
9954
|
+
const componentDiffs = diffResult.diffs.filter((d) => d.type === "component" && !skippedComponents.has(d.name));
|
|
9955
|
+
const componentResults = await Promise.allSettled(
|
|
9956
|
+
componentDiffs.map(async (diff) => {
|
|
9957
|
+
const localComp = local.components.find((c) => c.name === diff.name);
|
|
9958
|
+
if (diff.action === "create" && localComp) {
|
|
9959
|
+
await client.components.create({
|
|
9960
|
+
path: { space_id: spaceIdNum },
|
|
9961
|
+
body: { component: toComponentCreate(localComp) },
|
|
9962
|
+
throwOnError: true
|
|
9963
|
+
});
|
|
9964
|
+
return "created";
|
|
9965
|
+
}
|
|
9966
|
+
if (diff.action === "update" && localComp) {
|
|
9967
|
+
const existing = remote.components.get(diff.name);
|
|
9968
|
+
if (existing?.id) {
|
|
9969
|
+
await client.components.update(existing.id, {
|
|
9970
|
+
path: { space_id: spaceIdNum },
|
|
9971
|
+
body: { component: toComponentUpdate(localComp) },
|
|
9972
|
+
throwOnError: true
|
|
9973
|
+
});
|
|
9974
|
+
return "updated";
|
|
9975
|
+
}
|
|
9976
|
+
}
|
|
9977
|
+
})
|
|
9978
|
+
);
|
|
9979
|
+
for (let i = 0; i < componentResults.length; i++) {
|
|
9980
|
+
const result = componentResults[i];
|
|
9981
|
+
const diff = componentDiffs[i];
|
|
9982
|
+
if (result.status === "fulfilled") {
|
|
9983
|
+
if (result.value === "created") {
|
|
9984
|
+
created++;
|
|
9985
|
+
} else if (result.value === "updated") {
|
|
9986
|
+
updated++;
|
|
9987
|
+
}
|
|
9988
|
+
} else {
|
|
9989
|
+
const eventId = diff.action === "create" ? "push_component" : "update_component";
|
|
9990
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} component ${diff.name}`);
|
|
9991
|
+
}
|
|
9992
|
+
}
|
|
9993
|
+
const datasourceDiffs = diffResult.diffs.filter((d) => d.type === "datasource");
|
|
9994
|
+
const datasourceResults = await Promise.allSettled(
|
|
9995
|
+
datasourceDiffs.map(async (diff) => {
|
|
9996
|
+
const localDs = local.datasources.find((d) => d.name === diff.name);
|
|
9997
|
+
if (diff.action === "create" && localDs) {
|
|
9998
|
+
await client.datasources.create({
|
|
9999
|
+
path: { space_id: spaceIdNum },
|
|
10000
|
+
body: { datasource: toDatasourceCreate(localDs) },
|
|
10001
|
+
throwOnError: true
|
|
10002
|
+
});
|
|
10003
|
+
return "created";
|
|
10004
|
+
}
|
|
10005
|
+
if (diff.action === "update" && localDs) {
|
|
10006
|
+
const existing = remote.datasources.get(diff.name);
|
|
10007
|
+
if (existing?.id) {
|
|
10008
|
+
await client.datasources.update(existing.id, {
|
|
10009
|
+
path: { space_id: spaceIdNum },
|
|
10010
|
+
body: { datasource: toDatasourceUpdate(localDs, existing) },
|
|
10011
|
+
throwOnError: true
|
|
10012
|
+
});
|
|
10013
|
+
return "updated";
|
|
10014
|
+
}
|
|
10015
|
+
}
|
|
10016
|
+
})
|
|
10017
|
+
);
|
|
10018
|
+
for (let i = 0; i < datasourceResults.length; i++) {
|
|
10019
|
+
const result = datasourceResults[i];
|
|
10020
|
+
const diff = datasourceDiffs[i];
|
|
10021
|
+
if (result.status === "fulfilled") {
|
|
10022
|
+
if (result.value === "created") {
|
|
10023
|
+
created++;
|
|
10024
|
+
} else if (result.value === "updated") {
|
|
10025
|
+
updated++;
|
|
10026
|
+
}
|
|
10027
|
+
} else {
|
|
10028
|
+
const eventId = diff.action === "create" ? "push_datasource" : "update_datasource";
|
|
10029
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} datasource ${diff.name}`);
|
|
10030
|
+
}
|
|
10031
|
+
}
|
|
10032
|
+
if (options.delete) {
|
|
10033
|
+
const staleComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
|
|
10034
|
+
const deleteComponentResults = await Promise.allSettled(
|
|
10035
|
+
staleComponents.map(async (diff) => {
|
|
10036
|
+
const existing = remote.components.get(diff.name);
|
|
10037
|
+
if (existing?.id) {
|
|
10038
|
+
await client.components.delete(existing.id, {
|
|
10039
|
+
path: { space_id: spaceIdNum },
|
|
10040
|
+
throwOnError: true
|
|
10041
|
+
});
|
|
10042
|
+
return true;
|
|
10043
|
+
}
|
|
10044
|
+
})
|
|
10045
|
+
);
|
|
10046
|
+
for (let i = 0; i < deleteComponentResults.length; i++) {
|
|
10047
|
+
const result = deleteComponentResults[i];
|
|
10048
|
+
if (result.status === "fulfilled") {
|
|
10049
|
+
if (result.value) {
|
|
10050
|
+
deleted++;
|
|
10051
|
+
}
|
|
10052
|
+
} else {
|
|
10053
|
+
handleAPIError("push_component", result.reason, `Failed to delete component ${staleComponents[i].name}`);
|
|
10054
|
+
}
|
|
10055
|
+
}
|
|
10056
|
+
const staleDatasources = diffResult.diffs.filter((d) => d.type === "datasource" && d.action === "stale");
|
|
10057
|
+
const deleteDatasourceResults = await Promise.allSettled(
|
|
10058
|
+
staleDatasources.map(async (diff) => {
|
|
10059
|
+
const existing = remote.datasources.get(diff.name);
|
|
10060
|
+
if (existing?.id) {
|
|
10061
|
+
await client.datasources.delete(existing.id, {
|
|
10062
|
+
path: { space_id: spaceIdNum },
|
|
10063
|
+
throwOnError: true
|
|
10064
|
+
});
|
|
10065
|
+
return true;
|
|
10066
|
+
}
|
|
10067
|
+
})
|
|
10068
|
+
);
|
|
10069
|
+
for (let i = 0; i < deleteDatasourceResults.length; i++) {
|
|
10070
|
+
const result = deleteDatasourceResults[i];
|
|
10071
|
+
if (result.status === "fulfilled") {
|
|
10072
|
+
if (result.value) {
|
|
10073
|
+
deleted++;
|
|
10074
|
+
}
|
|
10075
|
+
} else {
|
|
10076
|
+
handleAPIError("delete_datasource", result.reason, `Failed to delete datasource ${staleDatasources[i].name}`);
|
|
10077
|
+
}
|
|
10078
|
+
}
|
|
10079
|
+
const staleFolders = diffResult.diffs.filter((d) => d.type === "componentFolder" && d.action === "stale");
|
|
10080
|
+
const deleteFolderResults = await Promise.allSettled(
|
|
10081
|
+
staleFolders.map(async (diff) => {
|
|
10082
|
+
const existing = remote.componentFolders.get(diff.name);
|
|
10083
|
+
if (existing?.id) {
|
|
10084
|
+
await client.componentFolders.delete(existing.id, {
|
|
10085
|
+
path: { space_id: spaceIdNum },
|
|
10086
|
+
throwOnError: true
|
|
10087
|
+
});
|
|
10088
|
+
return true;
|
|
10089
|
+
}
|
|
10090
|
+
})
|
|
10091
|
+
);
|
|
10092
|
+
for (let i = 0; i < deleteFolderResults.length; i++) {
|
|
10093
|
+
const result = deleteFolderResults[i];
|
|
10094
|
+
if (result.status === "fulfilled") {
|
|
10095
|
+
if (result.value) {
|
|
10096
|
+
deleted++;
|
|
10097
|
+
}
|
|
10098
|
+
} else {
|
|
10099
|
+
handleAPIError("push_component_group", result.reason, `Failed to delete folder ${staleFolders[i].name}`);
|
|
10100
|
+
}
|
|
10101
|
+
}
|
|
10102
|
+
}
|
|
10103
|
+
return { created, updated, deleted };
|
|
10104
|
+
}
|
|
10105
|
+
function buildChangesetEntries(diffResult, local, remote, options) {
|
|
10106
|
+
const changes = [];
|
|
10107
|
+
for (const diff of diffResult.diffs) {
|
|
10108
|
+
if (diff.action === "unchanged") {
|
|
10109
|
+
continue;
|
|
10110
|
+
}
|
|
10111
|
+
if (diff.action === "stale" && !options.delete) {
|
|
10112
|
+
continue;
|
|
10113
|
+
}
|
|
10114
|
+
const action = diff.action === "stale" ? "delete" : diff.action;
|
|
10115
|
+
let remoteSrc;
|
|
10116
|
+
let localSrc;
|
|
10117
|
+
if (diff.type === "component") {
|
|
10118
|
+
remoteSrc = remote.components.get(diff.name);
|
|
10119
|
+
localSrc = local.components.find((c) => c.name === diff.name);
|
|
10120
|
+
} else if (diff.type === "componentFolder") {
|
|
10121
|
+
remoteSrc = remote.componentFolders.get(diff.name);
|
|
10122
|
+
localSrc = local.componentFolders.find((f) => f.name === diff.name);
|
|
10123
|
+
} else if (diff.type === "datasource") {
|
|
10124
|
+
remoteSrc = remote.datasources.get(diff.name);
|
|
10125
|
+
localSrc = local.datasources.find((d) => d.name === diff.name);
|
|
10126
|
+
}
|
|
10127
|
+
changes.push({
|
|
10128
|
+
type: diff.type,
|
|
10129
|
+
name: diff.name,
|
|
10130
|
+
action,
|
|
10131
|
+
...remoteSrc && { before: { ...remoteSrc } },
|
|
10132
|
+
...localSrc && { after: { ...localSrc } }
|
|
10133
|
+
});
|
|
10134
|
+
}
|
|
10135
|
+
return changes;
|
|
10136
|
+
}
|
|
10137
|
+
|
|
10138
|
+
function findRemoteFolderByUuid(remoteFolders, uuid) {
|
|
10139
|
+
for (const folder of remoteFolders.values()) {
|
|
10140
|
+
if (folder.uuid === uuid) {
|
|
10141
|
+
return folder;
|
|
10142
|
+
}
|
|
10143
|
+
}
|
|
10144
|
+
return void 0;
|
|
10145
|
+
}
|
|
10146
|
+
function resolveFolderReferences(local, remote) {
|
|
10147
|
+
const localUuidToName = /* @__PURE__ */ new Map();
|
|
10148
|
+
for (const folder of local.componentFolders) {
|
|
10149
|
+
if (folder.uuid) {
|
|
10150
|
+
localUuidToName.set(folder.uuid, folder.name);
|
|
10151
|
+
}
|
|
10152
|
+
}
|
|
10153
|
+
const pendingFolderAssignments = /* @__PURE__ */ new Map();
|
|
10154
|
+
const resolvedComponents = local.components.map((comp) => {
|
|
10155
|
+
if (!comp.component_group_uuid) {
|
|
10156
|
+
return comp;
|
|
10157
|
+
}
|
|
10158
|
+
const folderName = localUuidToName.get(comp.component_group_uuid);
|
|
10159
|
+
if (!folderName) {
|
|
10160
|
+
return comp;
|
|
10161
|
+
}
|
|
10162
|
+
const remoteByUuid = findRemoteFolderByUuid(remote.componentFolders, comp.component_group_uuid);
|
|
10163
|
+
if (remoteByUuid) {
|
|
10164
|
+
return comp;
|
|
10165
|
+
}
|
|
10166
|
+
const remoteByName = remote.componentFolders.get(folderName);
|
|
10167
|
+
if (remoteByName) {
|
|
10168
|
+
return { ...comp, component_group_uuid: remoteByName.uuid };
|
|
10169
|
+
}
|
|
10170
|
+
const pending = pendingFolderAssignments.get(folderName) ?? [];
|
|
10171
|
+
pending.push(comp.name);
|
|
10172
|
+
pendingFolderAssignments.set(folderName, pending);
|
|
10173
|
+
return comp;
|
|
10174
|
+
});
|
|
10175
|
+
return {
|
|
10176
|
+
resolved: { ...local, components: resolvedComponents },
|
|
10177
|
+
pendingFolderAssignments
|
|
10178
|
+
};
|
|
10179
|
+
}
|
|
10180
|
+
|
|
10181
|
+
async function ensureDir(dir) {
|
|
10182
|
+
await mkdir(dir, { recursive: true });
|
|
10183
|
+
}
|
|
10184
|
+
async function saveChangeset(basePath, data) {
|
|
10185
|
+
const dir = join(basePath, "schema", "changesets");
|
|
10186
|
+
await ensureDir(dir);
|
|
10187
|
+
const fileName = `${fileTimestamp(data.timestamp)}.json`;
|
|
10188
|
+
const filePath = join(dir, fileName);
|
|
10189
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
10190
|
+
return filePath;
|
|
10191
|
+
}
|
|
10192
|
+
|
|
10193
|
+
const SENTINEL_FIELDS = /* @__PURE__ */ new Set(["_uid", "component"]);
|
|
10194
|
+
function classifyFieldChanges(remoteSchema, localSchema) {
|
|
10195
|
+
const removed = [];
|
|
10196
|
+
const added = [];
|
|
10197
|
+
const typeChanged = [];
|
|
10198
|
+
const requiredAdded = [];
|
|
10199
|
+
const requiredChanged = [];
|
|
10200
|
+
for (const [field, remoteField] of Object.entries(remoteSchema)) {
|
|
10201
|
+
if (SENTINEL_FIELDS.has(field)) {
|
|
10202
|
+
continue;
|
|
10203
|
+
}
|
|
10204
|
+
if (typeof remoteField.type !== "string") {
|
|
10205
|
+
continue;
|
|
10206
|
+
}
|
|
10207
|
+
if (!(field in localSchema)) {
|
|
10208
|
+
removed.push({ field, type: remoteField.type });
|
|
10209
|
+
}
|
|
10210
|
+
}
|
|
10211
|
+
for (const [field, localField] of Object.entries(localSchema)) {
|
|
10212
|
+
if (SENTINEL_FIELDS.has(field)) {
|
|
10213
|
+
continue;
|
|
10214
|
+
}
|
|
10215
|
+
if (typeof localField.type !== "string") {
|
|
10216
|
+
continue;
|
|
10217
|
+
}
|
|
10218
|
+
if (!(field in remoteSchema)) {
|
|
10219
|
+
if (localField.required) {
|
|
10220
|
+
requiredAdded.push({ field, type: localField.type });
|
|
10221
|
+
} else {
|
|
10222
|
+
added.push({ field, type: localField.type, required: false });
|
|
10223
|
+
}
|
|
10224
|
+
} else {
|
|
10225
|
+
const remoteField = remoteSchema[field];
|
|
10226
|
+
if (typeof remoteField?.type !== "string") {
|
|
10227
|
+
continue;
|
|
10228
|
+
}
|
|
10229
|
+
if (remoteField.type !== localField.type) {
|
|
10230
|
+
typeChanged.push({ field, oldType: remoteField.type, newType: localField.type });
|
|
10231
|
+
}
|
|
10232
|
+
if (localField.required && !remoteField.required) {
|
|
10233
|
+
requiredChanged.push({ field, type: localField.type });
|
|
10234
|
+
}
|
|
10235
|
+
}
|
|
10236
|
+
}
|
|
10237
|
+
return { removed, added, typeChanged, requiredAdded, requiredChanged };
|
|
10238
|
+
}
|
|
10239
|
+
function longestCommonSubstring(a, b) {
|
|
10240
|
+
let maxLen = 0;
|
|
10241
|
+
for (let i = 0; i < a.length; i++) {
|
|
10242
|
+
for (let j = 0; j < b.length; j++) {
|
|
10243
|
+
let len = 0;
|
|
10244
|
+
while (i + len < a.length && j + len < b.length && a[i + len] === b[j + len]) {
|
|
10245
|
+
len++;
|
|
10246
|
+
}
|
|
10247
|
+
if (len > maxLen) {
|
|
10248
|
+
maxLen = len;
|
|
10249
|
+
}
|
|
10250
|
+
}
|
|
10251
|
+
}
|
|
10252
|
+
return maxLen;
|
|
10253
|
+
}
|
|
10254
|
+
function nameSimilarity(a, b) {
|
|
10255
|
+
const longer = Math.max(a.length, b.length);
|
|
10256
|
+
if (longer === 0) {
|
|
10257
|
+
return 1;
|
|
10258
|
+
}
|
|
10259
|
+
return longestCommonSubstring(a, b) / longer;
|
|
10260
|
+
}
|
|
10261
|
+
function detectRenames(removed, added) {
|
|
10262
|
+
const renames = [];
|
|
10263
|
+
const usedRemoved = /* @__PURE__ */ new Set();
|
|
10264
|
+
const usedAdded = /* @__PURE__ */ new Set();
|
|
10265
|
+
const addedByType = /* @__PURE__ */ new Map();
|
|
10266
|
+
for (const addedField of added) {
|
|
10267
|
+
if (!addedByType.has(addedField.type)) {
|
|
10268
|
+
addedByType.set(addedField.type, []);
|
|
10269
|
+
}
|
|
10270
|
+
addedByType.get(addedField.type).push(addedField);
|
|
10271
|
+
}
|
|
10272
|
+
const isSinglePair = removed.length === 1 && added.length === 1;
|
|
10273
|
+
for (const removedField of removed) {
|
|
10274
|
+
const candidates = addedByType.get(removedField.type) ?? [];
|
|
10275
|
+
const availableCandidates = candidates.filter((c) => !usedAdded.has(c.field));
|
|
10276
|
+
if (availableCandidates.length === 0) {
|
|
10277
|
+
continue;
|
|
10278
|
+
}
|
|
10279
|
+
let bestCandidate = availableCandidates[0];
|
|
10280
|
+
let bestScore = nameSimilarity(removedField.field, bestCandidate.field);
|
|
10281
|
+
for (let i = 1; i < availableCandidates.length; i++) {
|
|
10282
|
+
const score = nameSimilarity(removedField.field, availableCandidates[i].field);
|
|
10283
|
+
if (score > bestScore) {
|
|
10284
|
+
bestScore = score;
|
|
10285
|
+
bestCandidate = availableCandidates[i];
|
|
10286
|
+
}
|
|
10287
|
+
}
|
|
10288
|
+
if (!isSinglePair && bestScore < 0.3) {
|
|
10289
|
+
continue;
|
|
10290
|
+
}
|
|
10291
|
+
renames.push({ oldField: removedField.field, newField: bestCandidate.field, fieldType: removedField.type });
|
|
10292
|
+
usedRemoved.add(removedField.field);
|
|
10293
|
+
usedAdded.add(bestCandidate.field);
|
|
10294
|
+
}
|
|
10295
|
+
const unmatchedRemoved = removed.filter((r) => !usedRemoved.has(r.field));
|
|
10296
|
+
const unmatchedAdded = added.filter((a) => !usedAdded.has(a.field));
|
|
10297
|
+
return { renames, unmatchedRemoved, unmatchedAdded };
|
|
10298
|
+
}
|
|
10299
|
+
function analyzeBreakingChanges(diffResult, local, remote) {
|
|
10300
|
+
const results = [];
|
|
10301
|
+
const updatedComponents = diffResult.diffs.filter(
|
|
10302
|
+
(d) => d.type === "component" && d.action === "update"
|
|
10303
|
+
);
|
|
10304
|
+
for (const diff of updatedComponents) {
|
|
10305
|
+
const localComp = local.components.find((c) => c.name === diff.name);
|
|
10306
|
+
const remoteComp = remote.components.get(diff.name);
|
|
10307
|
+
if (!localComp?.schema || !remoteComp?.schema) {
|
|
10308
|
+
continue;
|
|
10309
|
+
}
|
|
10310
|
+
const classification = classifyFieldChanges(
|
|
10311
|
+
remoteComp.schema,
|
|
10312
|
+
localComp.schema
|
|
10313
|
+
);
|
|
10314
|
+
const changes = [];
|
|
10315
|
+
const { renames, unmatchedRemoved } = detectRenames(classification.removed, classification.added);
|
|
10316
|
+
for (const rename of renames) {
|
|
10317
|
+
changes.push({ kind: "rename", field: rename.newField, oldField: rename.oldField });
|
|
10318
|
+
}
|
|
10319
|
+
for (const removed of unmatchedRemoved) {
|
|
10320
|
+
changes.push({ kind: "removed", field: removed.field });
|
|
10321
|
+
}
|
|
10322
|
+
for (const tc of classification.typeChanged) {
|
|
10323
|
+
changes.push({ kind: "type_changed", field: tc.field, oldType: tc.oldType, newType: tc.newType });
|
|
10324
|
+
}
|
|
10325
|
+
for (const ra of classification.requiredAdded) {
|
|
10326
|
+
changes.push({ kind: "required_added", field: ra.field, fieldType: ra.type });
|
|
10327
|
+
}
|
|
10328
|
+
for (const rc of classification.requiredChanged) {
|
|
10329
|
+
changes.push({ kind: "required_changed", field: rc.field, fieldType: rc.type });
|
|
10330
|
+
}
|
|
10331
|
+
if (changes.length > 0) {
|
|
10332
|
+
results.push({ componentName: diff.name, changes });
|
|
10333
|
+
}
|
|
10334
|
+
}
|
|
10335
|
+
return results;
|
|
10336
|
+
}
|
|
10337
|
+
|
|
10338
|
+
const COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["text:textarea", "textarea:text"]);
|
|
10339
|
+
function defaultForType(fieldType) {
|
|
10340
|
+
switch (fieldType) {
|
|
10341
|
+
case "text":
|
|
10342
|
+
case "textarea":
|
|
10343
|
+
case "markdown":
|
|
10344
|
+
return `''`;
|
|
10345
|
+
case "number":
|
|
10346
|
+
return "0";
|
|
10347
|
+
case "boolean":
|
|
10348
|
+
return "false";
|
|
10349
|
+
default:
|
|
10350
|
+
return null;
|
|
10351
|
+
}
|
|
10352
|
+
}
|
|
10353
|
+
function typeConversion(field, oldType, newType) {
|
|
10354
|
+
const key = `${oldType}:${newType}`;
|
|
10355
|
+
if (COMPATIBLE_TYPES.has(key)) {
|
|
10356
|
+
return null;
|
|
10357
|
+
}
|
|
10358
|
+
const accessor = `block.${field}`;
|
|
10359
|
+
switch (key) {
|
|
10360
|
+
case "text:number":
|
|
10361
|
+
return `${accessor} = Number(${accessor}) || 0;`;
|
|
10362
|
+
case "number:text":
|
|
10363
|
+
return `${accessor} = String(${accessor});`;
|
|
10364
|
+
case "text:boolean":
|
|
10365
|
+
return `${accessor} = !!${accessor};`;
|
|
10366
|
+
case "boolean:text":
|
|
10367
|
+
return `${accessor} = String(${accessor});`;
|
|
10368
|
+
default:
|
|
10369
|
+
return `${accessor}; // TODO: convert from ${oldType} to ${newType}`;
|
|
10370
|
+
}
|
|
10371
|
+
}
|
|
10372
|
+
function renderMigrationCode(changes) {
|
|
10373
|
+
const lines = [];
|
|
10374
|
+
lines.push(" // Review this migration before running it against your space.");
|
|
10375
|
+
lines.push(" // Generated migrations are scaffolds and may need manual adjustments.");
|
|
10376
|
+
lines.push(" // Example rename migration:");
|
|
10377
|
+
lines.push(" // block.new_field = block.old_field;");
|
|
10378
|
+
lines.push(" // delete block.old_field;");
|
|
10379
|
+
lines.push("");
|
|
10380
|
+
for (const change of changes) {
|
|
10381
|
+
switch (change.kind) {
|
|
10382
|
+
case "rename":
|
|
10383
|
+
lines.push(` // Rename: ${change.oldField} \u2192 ${change.field}`);
|
|
10384
|
+
lines.push(` if ('${change.oldField}' in block) {`);
|
|
10385
|
+
lines.push(` block.${change.field} = block.${change.oldField};`);
|
|
10386
|
+
lines.push(` delete block.${change.oldField};`);
|
|
10387
|
+
lines.push(` }`);
|
|
10388
|
+
break;
|
|
10389
|
+
case "removed":
|
|
10390
|
+
if (change.renameHint) {
|
|
10391
|
+
lines.push(` // If '${change.field}' was renamed to '${change.renameHint.newField}', uncomment:`);
|
|
10392
|
+
lines.push(` // block.${change.renameHint.newField} = block.${change.field};`);
|
|
10393
|
+
} else {
|
|
10394
|
+
lines.push(` // Removed field: ${change.field}`);
|
|
10395
|
+
}
|
|
10396
|
+
lines.push(` delete block.${change.field};`);
|
|
10397
|
+
break;
|
|
10398
|
+
case "type_changed": {
|
|
10399
|
+
const conversion = typeConversion(change.field, change.oldType, change.newType);
|
|
10400
|
+
if (conversion) {
|
|
10401
|
+
lines.push(` // Type change: ${change.field} (${change.oldType} \u2192 ${change.newType})`);
|
|
10402
|
+
lines.push(` ${conversion}`);
|
|
10403
|
+
}
|
|
10404
|
+
break;
|
|
10405
|
+
}
|
|
10406
|
+
case "required_added": {
|
|
10407
|
+
const defaultValue = defaultForType(change.fieldType);
|
|
10408
|
+
lines.push(` // New required field: ${change.field} (${change.fieldType})`);
|
|
10409
|
+
if (defaultValue !== null) {
|
|
10410
|
+
lines.push(` // TODO: provide a meaningful default value`);
|
|
10411
|
+
lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
|
|
10412
|
+
} else {
|
|
10413
|
+
lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
|
|
10414
|
+
lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
|
|
10415
|
+
}
|
|
10416
|
+
break;
|
|
10417
|
+
}
|
|
10418
|
+
case "required_changed": {
|
|
10419
|
+
const defaultValue = defaultForType(change.fieldType);
|
|
10420
|
+
lines.push(` // Field is now required: ${change.field} (${change.fieldType})`);
|
|
10421
|
+
lines.push(` // Existing stories may have null/undefined values \u2014 provide a default for those.`);
|
|
10422
|
+
if (defaultValue !== null) {
|
|
10423
|
+
lines.push(` // TODO: provide a meaningful default value`);
|
|
10424
|
+
lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
|
|
10425
|
+
} else {
|
|
10426
|
+
lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
|
|
10427
|
+
lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
|
|
10428
|
+
}
|
|
10429
|
+
break;
|
|
10430
|
+
}
|
|
10431
|
+
}
|
|
10432
|
+
lines.push("");
|
|
10433
|
+
}
|
|
10434
|
+
const body = lines.length > 0 ? `
|
|
10435
|
+
${lines.join("\n")}` : "\n";
|
|
10436
|
+
return `export default function (block) {${body} return block;
|
|
10437
|
+
}
|
|
10438
|
+
`;
|
|
10439
|
+
}
|
|
10440
|
+
async function writeMigrationFile(options) {
|
|
10441
|
+
const { spaceId, componentName, code, timestamp, basePath } = options;
|
|
10442
|
+
const dir = resolvePath(basePath, `migrations/${spaceId}`);
|
|
10443
|
+
await mkdir(dir, { recursive: true });
|
|
10444
|
+
const fileName = `${componentName}.${fileTimestamp(timestamp)}.js`;
|
|
10445
|
+
const filePath = join(dir, fileName);
|
|
10446
|
+
await writeFile(filePath, code, "utf-8");
|
|
10447
|
+
return filePath;
|
|
10448
|
+
}
|
|
10449
|
+
|
|
10450
|
+
const DEFAULT_GROUPS_FILENAME = "groups.json";
|
|
10451
|
+
const CONSOLIDATED_COMPONENTS_FILENAME = "components.json";
|
|
10452
|
+
async function writeLocalComponents({
|
|
10453
|
+
space,
|
|
10454
|
+
basePath,
|
|
10455
|
+
resolved,
|
|
10456
|
+
diffResult,
|
|
10457
|
+
deleteRemoved,
|
|
10458
|
+
ui,
|
|
10459
|
+
logger
|
|
10460
|
+
}) {
|
|
10461
|
+
const componentsDir = resolveCommandPath(directories.components, space, basePath);
|
|
10462
|
+
const consolidatedPath = join(componentsDir, CONSOLIDATED_COMPONENTS_FILENAME);
|
|
10463
|
+
if (await fileExists(consolidatedPath)) {
|
|
10464
|
+
ui.warn(
|
|
10465
|
+
`A consolidated ${CONSOLIDATED_COMPONENTS_FILENAME} exists at ${componentsDir}. Per-component files will still be written, but the consolidated file may shadow them when stories push validates schemas. Delete it or run \`storyblok components pull --separate-files\` to regenerate.`
|
|
10466
|
+
);
|
|
10467
|
+
}
|
|
10468
|
+
for (const component of resolved.components) {
|
|
10469
|
+
const filePath = join(componentsDir, `${sanitizeFilename(component.name || "")}.json`);
|
|
10470
|
+
await saveToFile(filePath, JSON.stringify(component, null, 2));
|
|
10471
|
+
}
|
|
10472
|
+
const groupsPath = join(componentsDir, DEFAULT_GROUPS_FILENAME);
|
|
10473
|
+
if (resolved.componentFolders.length > 0) {
|
|
10474
|
+
await saveToFile(groupsPath, JSON.stringify(resolved.componentFolders, null, 2));
|
|
10475
|
+
} else if (await fileExists(groupsPath)) {
|
|
10476
|
+
try {
|
|
10477
|
+
await unlink(groupsPath);
|
|
10478
|
+
logger.info("Removed stale local groups file", { path: groupsPath });
|
|
10479
|
+
} catch (error) {
|
|
10480
|
+
if (error.code !== "ENOENT") {
|
|
10481
|
+
throw error;
|
|
10482
|
+
}
|
|
10483
|
+
}
|
|
10484
|
+
}
|
|
10485
|
+
if (deleteRemoved) {
|
|
10486
|
+
const staleComponents = diffResult.diffs.filter(
|
|
10487
|
+
(d) => d.type === "component" && d.action === "stale"
|
|
10488
|
+
);
|
|
10489
|
+
for (const stale of staleComponents) {
|
|
10490
|
+
const filePath = join(componentsDir, `${sanitizeFilename(stale.name)}.json`);
|
|
10491
|
+
try {
|
|
10492
|
+
await unlink(filePath);
|
|
10493
|
+
logger.info("Removed stale local component file", { path: filePath });
|
|
10494
|
+
} catch (error) {
|
|
10495
|
+
if (error.code !== "ENOENT") {
|
|
10496
|
+
throw error;
|
|
10497
|
+
}
|
|
10498
|
+
}
|
|
10499
|
+
}
|
|
10500
|
+
}
|
|
10501
|
+
logger.info("Wrote local component files", {
|
|
10502
|
+
space,
|
|
10503
|
+
componentsWritten: resolved.components.length,
|
|
10504
|
+
groupsWritten: resolved.componentFolders.length
|
|
10505
|
+
});
|
|
10506
|
+
}
|
|
10507
|
+
|
|
10508
|
+
schemaCommand.command("push <entry-file>").description("Push local TypeScript schema and datasource definitions to a Storyblok space").option("-s, --space <space>", "space ID").option("-p, --path <path>", "path for file storage").option("--dry-run", "Show diffs without applying changes", false).option("--delete", "Delete remote entities not present in local schema", false).option("--migrations", "Generate scaffold migration files for breaking changes", true).addOption(new Option("--no-migrations", "Skip migration generation for breaking changes")).option("--write-components", "Write component schemas as local JSON files after push (also removes local files for components deleted via --delete)", true).addOption(new Option("--no-write-components", "Skip writing local component files")).action(async (entryFile, options, command) => {
|
|
10509
|
+
const ui = getUI();
|
|
10510
|
+
const logger = getLogger();
|
|
10511
|
+
const reporter = getReporter();
|
|
10512
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
10513
|
+
const { state } = session();
|
|
10514
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Pushing schema...");
|
|
10515
|
+
logger.info("Schema push started", { entryFile, space });
|
|
10516
|
+
if (!requireAuthentication(state, verbose)) {
|
|
10517
|
+
return;
|
|
10518
|
+
}
|
|
10519
|
+
if (!space) {
|
|
10520
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
10521
|
+
return;
|
|
10522
|
+
}
|
|
10523
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
10524
|
+
try {
|
|
10525
|
+
const loadSpinner = ui.createSpinner("Resolving schema...");
|
|
10526
|
+
let local;
|
|
10527
|
+
try {
|
|
10528
|
+
local = await loadSchema(entryFile);
|
|
10529
|
+
} catch (maybeError) {
|
|
10530
|
+
loadSpinner.failed("Failed to resolve schema");
|
|
10531
|
+
handleError(toError(maybeError), verbose);
|
|
10532
|
+
return;
|
|
10533
|
+
}
|
|
10534
|
+
loadSpinner.succeed(`Found: ${local.components.length} components, ${local.componentFolders.length} component folders, ${local.datasources.length} datasources`);
|
|
10535
|
+
const totalLocal = local.components.length + local.componentFolders.length + local.datasources.length;
|
|
10536
|
+
if (totalLocal === 0) {
|
|
10537
|
+
ui.warn("No components, folders, or datasources found in the entry file. Verify the file exports schema definitions.");
|
|
10538
|
+
return;
|
|
10539
|
+
}
|
|
10540
|
+
const remoteSpinner = ui.createSpinner(`Fetching remote state from space ${space}...`);
|
|
10541
|
+
let remoteResult;
|
|
10542
|
+
try {
|
|
10543
|
+
remoteResult = await fetchRemoteSchema(space);
|
|
10544
|
+
} catch (maybeError) {
|
|
10545
|
+
remoteSpinner.failed("Failed to fetch remote schema");
|
|
10546
|
+
handleError(toError(maybeError), verbose);
|
|
10547
|
+
return;
|
|
10548
|
+
}
|
|
10549
|
+
const { remote, rawComponents, rawComponentFolders, rawDatasources } = remoteResult;
|
|
10550
|
+
remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
|
|
10551
|
+
const { resolved, pendingFolderAssignments } = resolveFolderReferences(local, remote);
|
|
10552
|
+
const diffResult = diffSchema(resolved, remote);
|
|
10553
|
+
ui.br();
|
|
10554
|
+
ui.log(formatDiffOutput(diffResult, { delete: options.delete }));
|
|
10555
|
+
if (options.migrations) {
|
|
10556
|
+
const breakingChanges = analyzeBreakingChanges(diffResult, resolved, remote);
|
|
10557
|
+
if (breakingChanges.length > 0) {
|
|
10558
|
+
const totalChanges = breakingChanges.reduce((sum, c) => sum + c.changes.length, 0);
|
|
10559
|
+
ui.br();
|
|
10560
|
+
ui.warn(`${totalChanges} breaking change(s) detected in ${breakingChanges.length} component(s).`);
|
|
10561
|
+
ui.info("Generated migrations are scaffolds. Review and adjust them before running `storyblok migrations run`.");
|
|
10562
|
+
if (!options.dryRun) {
|
|
10563
|
+
const explicitMigrations = command.getOptionValueSource("migrations") === "cli";
|
|
10564
|
+
const shouldGenerate = explicitMigrations || await confirm({
|
|
10565
|
+
message: "Generate migration files for breaking changes?",
|
|
10566
|
+
default: true
|
|
10567
|
+
});
|
|
10568
|
+
if (shouldGenerate) {
|
|
10569
|
+
const migrationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10570
|
+
const resolvedBase = resolvePath(basePath, "");
|
|
10571
|
+
for (const comp of breakingChanges) {
|
|
10572
|
+
const renames = comp.changes.filter((c) => c.kind === "rename");
|
|
10573
|
+
if (renames.length > 0 && explicitMigrations) {
|
|
10574
|
+
for (const r of renames) {
|
|
10575
|
+
if (r.kind === "rename") {
|
|
10576
|
+
ui.log(` Assumed rename in '${comp.componentName}': ${r.oldField} \u2192 ${r.field}`);
|
|
10577
|
+
}
|
|
10578
|
+
}
|
|
10579
|
+
}
|
|
10580
|
+
if (renames.length > 0 && !explicitMigrations) {
|
|
10581
|
+
ui.br();
|
|
10582
|
+
ui.log(`Detected renames in '${comp.componentName}':`);
|
|
10583
|
+
for (const r of renames) {
|
|
10584
|
+
if (r.kind === "rename") {
|
|
10585
|
+
ui.log(` ${r.oldField} \u2192 ${r.field}`);
|
|
10586
|
+
}
|
|
10587
|
+
}
|
|
10588
|
+
const renameConfirmed = await confirm({
|
|
10589
|
+
message: "Are these renames correct?",
|
|
10590
|
+
default: true
|
|
10591
|
+
});
|
|
10592
|
+
if (!renameConfirmed) {
|
|
10593
|
+
comp.changes = comp.changes.map((c) => {
|
|
10594
|
+
if (c.kind === "rename") {
|
|
10595
|
+
return { kind: "removed", field: c.oldField, renameHint: { newField: c.field } };
|
|
10596
|
+
}
|
|
10597
|
+
return c;
|
|
10598
|
+
});
|
|
10599
|
+
}
|
|
10600
|
+
}
|
|
10601
|
+
const code = renderMigrationCode(comp.changes);
|
|
10602
|
+
const path = await writeMigrationFile({
|
|
10603
|
+
spaceId: space,
|
|
10604
|
+
componentName: comp.componentName,
|
|
10605
|
+
code,
|
|
10606
|
+
timestamp: migrationTimestamp,
|
|
10607
|
+
basePath: resolvedBase
|
|
10608
|
+
});
|
|
10609
|
+
ui.log(` Generated: ${path}`);
|
|
10610
|
+
}
|
|
10611
|
+
ui.br();
|
|
10612
|
+
ui.info(`Run migrations when ready: storyblok migrations run --space ${space}`);
|
|
10613
|
+
}
|
|
10614
|
+
}
|
|
10615
|
+
}
|
|
10616
|
+
}
|
|
10617
|
+
if (options.delete) {
|
|
10618
|
+
const deletedComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
|
|
10619
|
+
for (const comp of deletedComponents) {
|
|
10620
|
+
ui.warn(`Component '${comp.name}' will be deleted. Stories using it will have out-of-schema content.`);
|
|
10621
|
+
}
|
|
10622
|
+
}
|
|
10623
|
+
if (diffResult.stale > 0 && !options.delete) {
|
|
10624
|
+
ui.warn(`${diffResult.stale} stale entity(s) exist remotely but not in schema. Use --delete to remove.`);
|
|
10625
|
+
}
|
|
10626
|
+
if (options.dryRun) {
|
|
10627
|
+
ui.info("Dry run \u2014 no changes applied.");
|
|
10628
|
+
logger.info("Dry run completed", { creates: diffResult.creates, updates: diffResult.updates });
|
|
10629
|
+
return;
|
|
10630
|
+
}
|
|
10631
|
+
const resolvedPath = resolvePath(basePath, "");
|
|
10632
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10633
|
+
const changesetPath = await saveChangeset(resolvedPath, {
|
|
10634
|
+
timestamp,
|
|
10635
|
+
spaceId: Number(space),
|
|
10636
|
+
remote: { components: rawComponents, componentFolders: rawComponentFolders, datasources: rawDatasources },
|
|
10637
|
+
changes: buildChangesetEntries(diffResult, resolved, remote, { delete: options.delete })
|
|
10638
|
+
});
|
|
10639
|
+
logger.info("Changeset saved", { path: changesetPath });
|
|
10640
|
+
const nothingToPush = diffResult.creates === 0 && diffResult.updates === 0 && (!options.delete || diffResult.stale === 0);
|
|
10641
|
+
if (nothingToPush) {
|
|
10642
|
+
ui.ok("Everything up to date \u2014 nothing to push.");
|
|
10643
|
+
} else {
|
|
10644
|
+
const pushSpinner = ui.createSpinner("Pushing schema...");
|
|
10645
|
+
let result;
|
|
10646
|
+
try {
|
|
10647
|
+
result = await executePush(space, resolved, remote, diffResult, { delete: options.delete, pendingFolderAssignments });
|
|
10648
|
+
} catch (error) {
|
|
10649
|
+
pushSpinner.failed("Failed to push schema");
|
|
10650
|
+
throw error;
|
|
10651
|
+
}
|
|
10652
|
+
summary.total = result.created + result.updated + result.deleted;
|
|
10653
|
+
summary.succeeded = summary.total;
|
|
10654
|
+
pushSpinner.succeed(`Pushed ${result.created} creations, ${result.updated} updates${result.deleted > 0 ? `, ${result.deleted} deletions` : ""}.`);
|
|
10655
|
+
}
|
|
10656
|
+
if (options.writeComponents) {
|
|
10657
|
+
try {
|
|
10658
|
+
await writeLocalComponents({
|
|
10659
|
+
space,
|
|
10660
|
+
basePath,
|
|
10661
|
+
resolved,
|
|
10662
|
+
diffResult,
|
|
10663
|
+
deleteRemoved: options.delete,
|
|
10664
|
+
ui,
|
|
10665
|
+
logger
|
|
10666
|
+
});
|
|
10667
|
+
} catch (writeError) {
|
|
10668
|
+
ui.warn(`Failed to write local component files: ${toError(writeError).message}`);
|
|
10669
|
+
logger.warn("Failed to write local component files", { error: toError(writeError).message });
|
|
10670
|
+
}
|
|
10671
|
+
}
|
|
10672
|
+
} catch (maybeError) {
|
|
10673
|
+
summary.failed += 1;
|
|
10674
|
+
handleError(toError(maybeError), verbose);
|
|
10675
|
+
} finally {
|
|
10676
|
+
logger.info("Schema push finished", { summary });
|
|
10677
|
+
reporter.addSummary("schemaPushResults", summary);
|
|
10678
|
+
reporter.finalize();
|
|
10679
|
+
}
|
|
10680
|
+
});
|
|
10681
|
+
|
|
10682
|
+
const FIELD_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "pos"]);
|
|
10683
|
+
function toCamelCase(str) {
|
|
10684
|
+
return str.toLowerCase().replace(/[\s_-]+(.)/g, (_, char) => char.toUpperCase());
|
|
10685
|
+
}
|
|
10686
|
+
function toKebabCase(str) {
|
|
10687
|
+
return str.replace(/[\s_]+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
10688
|
+
}
|
|
10689
|
+
function componentVarName(name) {
|
|
10690
|
+
return `${toCamelCase(name)}Block`;
|
|
10691
|
+
}
|
|
10692
|
+
function folderVarName(name) {
|
|
10693
|
+
return `${toCamelCase(name)}Folder`;
|
|
10694
|
+
}
|
|
10695
|
+
function datasourceVarName(name) {
|
|
10696
|
+
return `${toCamelCase(name)}Datasource`;
|
|
10697
|
+
}
|
|
10698
|
+
function componentFileName(name) {
|
|
10699
|
+
return toKebabCase(name);
|
|
10700
|
+
}
|
|
10701
|
+
function folderFileName(name) {
|
|
10702
|
+
return toKebabCase(name);
|
|
10703
|
+
}
|
|
10704
|
+
function datasourceFileName(datasource) {
|
|
10705
|
+
return toKebabCase(datasource.slug || datasource.name);
|
|
10706
|
+
}
|
|
10707
|
+
function generateFieldCode(fieldName, fieldData, depth) {
|
|
10708
|
+
const clean = stripKeys(fieldData, FIELD_STRIP_KEYS);
|
|
10709
|
+
return `defineField('${fieldName.replace(/'/g, "\\'")}', ${formatValue(clean, depth + 1)})`;
|
|
10710
|
+
}
|
|
10711
|
+
function sortSchemaByPos(schema) {
|
|
10712
|
+
return Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
|
|
10713
|
+
const posA = typeof a.pos === "number" ? a.pos : Infinity;
|
|
10714
|
+
const posB = typeof b.pos === "number" ? b.pos : Infinity;
|
|
10715
|
+
return posA - posB;
|
|
10716
|
+
});
|
|
10717
|
+
}
|
|
10718
|
+
function generateComponentFile(component, componentFolders) {
|
|
10719
|
+
const lines = [];
|
|
10720
|
+
let matchedFolder;
|
|
10721
|
+
if (component.component_group_uuid && componentFolders) {
|
|
10722
|
+
matchedFolder = componentFolders.find((f) => f.uuid === component.component_group_uuid);
|
|
10723
|
+
}
|
|
10724
|
+
lines.push("import {");
|
|
10725
|
+
lines.push(" defineBlock,");
|
|
10726
|
+
lines.push(" defineField,");
|
|
10727
|
+
lines.push("} from '@storyblok/schema';");
|
|
10728
|
+
if (matchedFolder) {
|
|
10729
|
+
lines.push("");
|
|
10730
|
+
const fVarName = folderVarName(matchedFolder.name);
|
|
10731
|
+
const fFileName = folderFileName(matchedFolder.name);
|
|
10732
|
+
lines.push(`import { ${fVarName} } from './folders/${fFileName}';`);
|
|
10733
|
+
}
|
|
10734
|
+
lines.push("");
|
|
10735
|
+
const varName = componentVarName(component.name);
|
|
10736
|
+
lines.push(`export const ${varName} = defineBlock({`);
|
|
10737
|
+
const clean = stripKeys(component, COMPONENT_STRIP_KEYS);
|
|
10738
|
+
if (matchedFolder) {
|
|
10739
|
+
delete clean.component_group_uuid;
|
|
10740
|
+
}
|
|
10741
|
+
const orderedKeys = [];
|
|
10742
|
+
if (clean.name !== void 0) {
|
|
10743
|
+
orderedKeys.push("name");
|
|
10744
|
+
}
|
|
10745
|
+
if (clean.display_name !== void 0) {
|
|
10746
|
+
orderedKeys.push("display_name");
|
|
10747
|
+
}
|
|
10748
|
+
if (clean.is_root !== void 0) {
|
|
10749
|
+
orderedKeys.push("is_root");
|
|
10750
|
+
}
|
|
10751
|
+
if (clean.is_nestable !== void 0) {
|
|
10752
|
+
orderedKeys.push("is_nestable");
|
|
10753
|
+
}
|
|
10754
|
+
const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
|
|
10755
|
+
for (const key of Object.keys(clean).sort()) {
|
|
10756
|
+
if (!handled.has(key)) {
|
|
10757
|
+
orderedKeys.push(key);
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
if (matchedFolder) {
|
|
10761
|
+
const fVarName = folderVarName(matchedFolder.name);
|
|
10762
|
+
lines.push(`${INDENT}component_group_uuid: ${fVarName}.uuid,`);
|
|
10763
|
+
}
|
|
10764
|
+
for (const key of orderedKeys) {
|
|
10765
|
+
lines.push(`${INDENT}${key}: ${formatValue(clean[key], 1)},`);
|
|
10766
|
+
}
|
|
10767
|
+
if (clean.schema && typeof clean.schema === "object") {
|
|
10768
|
+
const schema = clean.schema;
|
|
10769
|
+
const sortedFields = sortSchemaByPos(schema);
|
|
10770
|
+
if (sortedFields.length > 0) {
|
|
10771
|
+
lines.push(`${INDENT}schema: [`);
|
|
10772
|
+
for (const [fieldName, fieldData] of sortedFields) {
|
|
10773
|
+
const fieldCode = generateFieldCode(fieldName, fieldData, 2);
|
|
10774
|
+
lines.push(`${INDENT}${INDENT}${fieldCode},`);
|
|
10775
|
+
}
|
|
10776
|
+
lines.push(`${INDENT}],`);
|
|
10777
|
+
} else {
|
|
10778
|
+
lines.push(`${INDENT}schema: [],`);
|
|
10779
|
+
}
|
|
10780
|
+
}
|
|
10781
|
+
lines.push("});");
|
|
10782
|
+
lines.push("");
|
|
10783
|
+
return lines.join("\n");
|
|
10784
|
+
}
|
|
10785
|
+
function generateFolderFile(folder) {
|
|
10786
|
+
const lines = [];
|
|
10787
|
+
lines.push("import { defineBlockFolder } from '@storyblok/schema';");
|
|
10788
|
+
lines.push("");
|
|
10789
|
+
const varName = folderVarName(folder.name);
|
|
10790
|
+
lines.push(`export const ${varName} = defineBlockFolder({`);
|
|
10791
|
+
const clean = stripKeys(folder, FOLDER_INIT_STRIP_KEYS);
|
|
10792
|
+
if (clean.name !== void 0) {
|
|
10793
|
+
lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
|
|
10794
|
+
}
|
|
10795
|
+
const handled = /* @__PURE__ */ new Set(["name"]);
|
|
10796
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
10797
|
+
if (!handled.has(key)) {
|
|
10798
|
+
lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
|
|
10799
|
+
}
|
|
10800
|
+
}
|
|
10801
|
+
lines.push("});");
|
|
10802
|
+
lines.push("");
|
|
10803
|
+
return lines.join("\n");
|
|
10804
|
+
}
|
|
10805
|
+
function generateDatasourceFile(datasource) {
|
|
10806
|
+
const lines = [];
|
|
10807
|
+
lines.push("import { defineDatasource } from '@storyblok/schema';");
|
|
10808
|
+
lines.push("");
|
|
10809
|
+
const varName = datasourceVarName(datasource.name);
|
|
10810
|
+
lines.push(`export const ${varName} = defineDatasource({`);
|
|
10811
|
+
const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
|
|
10812
|
+
if (clean.name !== void 0) {
|
|
10813
|
+
lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
|
|
10814
|
+
}
|
|
10815
|
+
if (clean.slug !== void 0) {
|
|
10816
|
+
lines.push(`${INDENT}slug: ${formatValue(clean.slug, 1)},`);
|
|
10817
|
+
}
|
|
10818
|
+
const handled = /* @__PURE__ */ new Set(["name", "slug"]);
|
|
10819
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
10820
|
+
if (!handled.has(key)) {
|
|
10821
|
+
lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
|
|
10822
|
+
}
|
|
10823
|
+
}
|
|
10824
|
+
lines.push("});");
|
|
10825
|
+
lines.push("");
|
|
10826
|
+
return lines.join("\n");
|
|
10827
|
+
}
|
|
10828
|
+
function generateSchemaFile(components, componentFolders, datasources) {
|
|
10829
|
+
const lines = [];
|
|
10830
|
+
lines.push("import type { Schema as InferSchema, Story as InferStory } from '@storyblok/schema';");
|
|
10831
|
+
lines.push("import type { MapiStory as InferStoryMapi } from '@storyblok/schema';");
|
|
10832
|
+
lines.push("");
|
|
10833
|
+
for (const component of components) {
|
|
10834
|
+
const varName = componentVarName(component.name);
|
|
10835
|
+
const fileName = componentFileName(component.name);
|
|
10836
|
+
lines.push(`import { ${varName} } from './components/${fileName}';`);
|
|
10837
|
+
}
|
|
10838
|
+
for (const folder of componentFolders) {
|
|
10839
|
+
const varName = folderVarName(folder.name);
|
|
10840
|
+
const fileName = folderFileName(folder.name);
|
|
10841
|
+
lines.push(`import { ${varName} } from './components/folders/${fileName}';`);
|
|
10842
|
+
}
|
|
10843
|
+
for (const datasource of datasources) {
|
|
10844
|
+
const varName = datasourceVarName(datasource.name);
|
|
10845
|
+
const fileName = datasourceFileName(datasource);
|
|
10846
|
+
lines.push(`import { ${varName} } from './datasources/${fileName}';`);
|
|
10847
|
+
}
|
|
10848
|
+
lines.push("");
|
|
10849
|
+
lines.push("export const schema = {");
|
|
10850
|
+
if (components.length > 0) {
|
|
10851
|
+
lines.push(" blocks: {");
|
|
10852
|
+
for (const component of components) {
|
|
10853
|
+
const varName = componentVarName(component.name);
|
|
10854
|
+
lines.push(` ${varName},`);
|
|
10855
|
+
}
|
|
10856
|
+
lines.push(" },");
|
|
10857
|
+
}
|
|
10858
|
+
if (componentFolders.length > 0) {
|
|
10859
|
+
lines.push(" blockFolders: {");
|
|
10860
|
+
for (const folder of componentFolders) {
|
|
10861
|
+
const varName = folderVarName(folder.name);
|
|
10862
|
+
lines.push(` ${varName},`);
|
|
10863
|
+
}
|
|
10864
|
+
lines.push(" },");
|
|
10865
|
+
}
|
|
10866
|
+
if (datasources.length > 0) {
|
|
10867
|
+
lines.push(" datasources: {");
|
|
10868
|
+
for (const datasource of datasources) {
|
|
10869
|
+
const varName = datasourceVarName(datasource.name);
|
|
10870
|
+
lines.push(` ${varName},`);
|
|
10871
|
+
}
|
|
10872
|
+
lines.push(" },");
|
|
10873
|
+
}
|
|
10874
|
+
lines.push("};");
|
|
10875
|
+
lines.push("");
|
|
10876
|
+
lines.push("export type Schema = InferSchema<typeof schema>;");
|
|
10877
|
+
lines.push("export type Blocks = Schema['blocks'];");
|
|
10878
|
+
lines.push("export type Story = InferStory<Blocks>;");
|
|
10879
|
+
lines.push("export type StoryMapi = InferStoryMapi<Blocks>;");
|
|
10880
|
+
lines.push("");
|
|
10881
|
+
return lines.join("\n");
|
|
10882
|
+
}
|
|
10883
|
+
|
|
10884
|
+
async function writeFileWithDirs(filePath, content) {
|
|
10885
|
+
const dir = dirname(filePath);
|
|
10886
|
+
await mkdir(dir, { recursive: true });
|
|
10887
|
+
await writeFile(filePath, content, "utf-8");
|
|
10888
|
+
}
|
|
10889
|
+
async function writeSchemaFiles(targetPath, components, componentFolders, datasources) {
|
|
10890
|
+
const writtenFiles = [];
|
|
10891
|
+
for (const comp of components) {
|
|
10892
|
+
const fileName = componentFileName(comp.name);
|
|
10893
|
+
const filePath = join(targetPath, "components", `${fileName}.ts`);
|
|
10894
|
+
await writeFileWithDirs(filePath, generateComponentFile(comp, componentFolders));
|
|
10895
|
+
writtenFiles.push(filePath);
|
|
10896
|
+
}
|
|
10897
|
+
for (const folder of componentFolders) {
|
|
10898
|
+
const fileName = folderFileName(folder.name);
|
|
10899
|
+
const filePath = join(targetPath, "components", "folders", `${fileName}.ts`);
|
|
10900
|
+
await writeFileWithDirs(filePath, generateFolderFile(folder));
|
|
10901
|
+
writtenFiles.push(filePath);
|
|
10902
|
+
}
|
|
10903
|
+
for (const ds of datasources) {
|
|
10904
|
+
const fileName = datasourceFileName(ds);
|
|
10905
|
+
const filePath = join(targetPath, "datasources", `${fileName}.ts`);
|
|
10906
|
+
await writeFileWithDirs(filePath, generateDatasourceFile(ds));
|
|
10907
|
+
writtenFiles.push(filePath);
|
|
10908
|
+
}
|
|
10909
|
+
const schemaPath = join(targetPath, "schema.ts");
|
|
10910
|
+
await writeFileWithDirs(schemaPath, generateSchemaFile(components, componentFolders, datasources));
|
|
10911
|
+
writtenFiles.push(schemaPath);
|
|
10912
|
+
return writtenFiles;
|
|
10913
|
+
}
|
|
10914
|
+
|
|
10915
|
+
async function isTargetEmpty(targetPath) {
|
|
10916
|
+
try {
|
|
10917
|
+
const entries = await readdir(targetPath);
|
|
10918
|
+
return entries.every((entry) => entry.startsWith("."));
|
|
10919
|
+
} catch (maybeError) {
|
|
10920
|
+
const error = maybeError;
|
|
10921
|
+
if (error?.code === "ENOENT") {
|
|
10922
|
+
return true;
|
|
10923
|
+
}
|
|
10924
|
+
throw error;
|
|
10925
|
+
}
|
|
10926
|
+
}
|
|
10927
|
+
schemaCommand.command("init").description("Initialize a local code-driven schema workspace from an existing Storyblok space (one-time bootstrap)").option("-s, --space <space>", "space ID").option("--out-dir <dir>", "Output directory for generated bootstrap files", ".storyblok/schema").action(async (options, command) => {
|
|
10928
|
+
const ui = getUI();
|
|
10929
|
+
const logger = getLogger();
|
|
10930
|
+
const reporter = getReporter();
|
|
10931
|
+
const { space, verbose } = command.optsWithGlobals();
|
|
10932
|
+
const { state } = session();
|
|
10933
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Initializing schema...");
|
|
10934
|
+
logger.info("Schema init started", { space });
|
|
10935
|
+
if (!requireAuthentication(state, verbose)) {
|
|
10936
|
+
return;
|
|
10937
|
+
}
|
|
10938
|
+
if (!space) {
|
|
10939
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
10940
|
+
return;
|
|
10941
|
+
}
|
|
10942
|
+
const targetPath = resolve(options.outDir);
|
|
10943
|
+
if (!await isTargetEmpty(targetPath)) {
|
|
10944
|
+
handleError(
|
|
10945
|
+
new CommandError(`Target directory ${targetPath} is not empty. \`schema init\` is a one-time bootstrap and refuses to overwrite existing files. Use \`schema push\` for ongoing changes, or remove the directory to re-bootstrap.`),
|
|
10946
|
+
verbose
|
|
10947
|
+
);
|
|
10948
|
+
return;
|
|
10949
|
+
}
|
|
10950
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
10951
|
+
try {
|
|
10952
|
+
const fetchSpinner = ui.createSpinner(`Fetching schema from space ${space}...`);
|
|
10953
|
+
let fetchResult;
|
|
10954
|
+
try {
|
|
10955
|
+
fetchResult = await fetchRemoteSchema(space);
|
|
10956
|
+
} catch (maybeError) {
|
|
10957
|
+
fetchSpinner.failed("Failed to fetch remote schema");
|
|
10958
|
+
handleError(toError(maybeError), verbose);
|
|
10959
|
+
return;
|
|
10960
|
+
}
|
|
10961
|
+
const { rawComponents, rawComponentFolders, rawDatasources } = fetchResult;
|
|
10962
|
+
fetchSpinner.succeed(`Found: ${rawComponents.length} components, ${rawComponentFolders.length} component folders, ${rawDatasources.length} datasources`);
|
|
10963
|
+
const writeSpinner = ui.createSpinner(`Generating TypeScript files to ${targetPath}...`);
|
|
10964
|
+
const writtenFiles = await writeSchemaFiles(targetPath, rawComponents, rawComponentFolders, rawDatasources);
|
|
10965
|
+
summary.total = writtenFiles.length;
|
|
10966
|
+
summary.succeeded = writtenFiles.length;
|
|
10967
|
+
writeSpinner.succeed(`Generated ${writtenFiles.length} files`);
|
|
10968
|
+
ui.list(writtenFiles);
|
|
10969
|
+
ui.warn("`schema init` is a one-time bootstrap step for adopting an existing space. Review generated files before continuing.");
|
|
10970
|
+
ui.info("After bootstrapping, keep your local schema as the source of truth and use `schema push` for ongoing changes.");
|
|
10971
|
+
ui.info("Make sure `@storyblok/schema` is installed in the project that imports these files (e.g. `pnpm add @storyblok/schema`).");
|
|
10972
|
+
} catch (maybeError) {
|
|
10973
|
+
summary.failed += 1;
|
|
10974
|
+
handleError(toError(maybeError), verbose);
|
|
10975
|
+
} finally {
|
|
10976
|
+
logger.info("Schema init finished", { summary });
|
|
10977
|
+
reporter.addSummary("schemaInitResults", summary);
|
|
10978
|
+
reporter.finalize();
|
|
10979
|
+
}
|
|
10980
|
+
});
|
|
10981
|
+
|
|
10982
|
+
const API_ASSIGNED_FIELDS = [
|
|
10983
|
+
"id",
|
|
10984
|
+
"created_at",
|
|
10985
|
+
"updated_at",
|
|
10986
|
+
"real_name",
|
|
10987
|
+
"all_presets",
|
|
10988
|
+
"image",
|
|
10989
|
+
"uuid"
|
|
10990
|
+
];
|
|
10991
|
+
function stripApiFields(payload) {
|
|
10992
|
+
const result = { ...payload };
|
|
10993
|
+
for (const field of API_ASSIGNED_FIELDS) {
|
|
10994
|
+
delete result[field];
|
|
10995
|
+
}
|
|
10996
|
+
return result;
|
|
10997
|
+
}
|
|
10998
|
+
async function listChangesets(basePath) {
|
|
10999
|
+
const dir = join(basePath, "schema", "changesets");
|
|
11000
|
+
if (!await fileExists(dir)) {
|
|
11001
|
+
return [];
|
|
11002
|
+
}
|
|
11003
|
+
const files = await readDirectory(dir);
|
|
11004
|
+
return files.filter((f) => f.endsWith(".json")).sort().reverse().map((f) => join(dir, f));
|
|
11005
|
+
}
|
|
11006
|
+
async function loadChangeset(filePath) {
|
|
11007
|
+
const content = await readFile$1(filePath, "utf-8");
|
|
11008
|
+
return JSON.parse(content);
|
|
11009
|
+
}
|
|
11010
|
+
function buildRollbackOps(changeset) {
|
|
11011
|
+
if (changeset.changes.length === 0) {
|
|
11012
|
+
return [];
|
|
11013
|
+
}
|
|
11014
|
+
return changeset.changes.map((entry) => {
|
|
11015
|
+
switch (entry.action) {
|
|
11016
|
+
case "create":
|
|
11017
|
+
return { type: entry.type, name: entry.name, action: "delete", payload: {} };
|
|
11018
|
+
case "update":
|
|
11019
|
+
return { type: entry.type, name: entry.name, action: "update", payload: entry.before ?? {} };
|
|
11020
|
+
case "delete":
|
|
11021
|
+
return { type: entry.type, name: entry.name, action: "create", payload: entry.before ?? {} };
|
|
11022
|
+
default:
|
|
11023
|
+
return { type: entry.type, name: entry.name, action: entry.action, payload: {} };
|
|
11024
|
+
}
|
|
11025
|
+
});
|
|
11026
|
+
}
|
|
11027
|
+
function rollbackAction(original) {
|
|
11028
|
+
switch (original) {
|
|
11029
|
+
case "create":
|
|
11030
|
+
return "delete";
|
|
11031
|
+
case "update":
|
|
11032
|
+
return "update";
|
|
11033
|
+
case "delete":
|
|
11034
|
+
return "create";
|
|
11035
|
+
}
|
|
11036
|
+
}
|
|
11037
|
+
function formatRollbackOutput(changes) {
|
|
11038
|
+
const byType = {
|
|
11039
|
+
component: [],
|
|
11040
|
+
componentFolder: [],
|
|
11041
|
+
datasource: []
|
|
11042
|
+
};
|
|
11043
|
+
for (const entry of changes) {
|
|
11044
|
+
byType[entry.type]?.push(entry);
|
|
11045
|
+
}
|
|
11046
|
+
const icons = {
|
|
11047
|
+
create: chalk.green("+"),
|
|
11048
|
+
update: chalk.yellow("~"),
|
|
11049
|
+
delete: chalk.red("-")
|
|
11050
|
+
};
|
|
11051
|
+
const lines = [];
|
|
11052
|
+
const sections = [
|
|
11053
|
+
["Components", byType.component],
|
|
11054
|
+
["Component Folders", byType.componentFolder],
|
|
11055
|
+
["Datasources", byType.datasource]
|
|
11056
|
+
];
|
|
11057
|
+
for (const [label, entries] of sections) {
|
|
11058
|
+
if (entries.length === 0) {
|
|
11059
|
+
continue;
|
|
11060
|
+
}
|
|
11061
|
+
lines.push(chalk.bold(label));
|
|
11062
|
+
for (const entry of entries) {
|
|
11063
|
+
const action = rollbackAction(entry.action);
|
|
11064
|
+
const icon = icons[action] ?? " ";
|
|
11065
|
+
const name = action === "delete" ? chalk.red(entry.name) : entry.name;
|
|
11066
|
+
lines.push(` ${icon} ${name} ${chalk.dim(`(${action})`)}`);
|
|
11067
|
+
if (entry.action === "update" && entry.before && entry.after) {
|
|
11068
|
+
let fromStr;
|
|
11069
|
+
let toStr;
|
|
11070
|
+
if (entry.type === "component") {
|
|
11071
|
+
fromStr = serializeComponent(applyDefaults(entry.after, COMPONENT_DEFAULTS));
|
|
11072
|
+
toStr = serializeComponent(applyDefaults(entry.before, COMPONENT_DEFAULTS));
|
|
11073
|
+
} else if (entry.type === "componentFolder") {
|
|
11074
|
+
fromStr = serializeComponentFolder(entry.after);
|
|
11075
|
+
toStr = serializeComponentFolder(entry.before);
|
|
11076
|
+
} else {
|
|
11077
|
+
fromStr = serializeDatasource(entry.after);
|
|
11078
|
+
toStr = serializeDatasource(entry.before);
|
|
11079
|
+
}
|
|
11080
|
+
if (fromStr !== toStr) {
|
|
11081
|
+
const patch = createTwoFilesPatch(
|
|
11082
|
+
`current/${entry.name}`,
|
|
11083
|
+
`restore/${entry.name}`,
|
|
11084
|
+
fromStr,
|
|
11085
|
+
toStr,
|
|
11086
|
+
"current",
|
|
11087
|
+
"restore"
|
|
11088
|
+
);
|
|
11089
|
+
for (const line of patch.split("\n")) {
|
|
11090
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
11091
|
+
lines.push(` ${chalk.green(line)}`);
|
|
11092
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
11093
|
+
lines.push(` ${chalk.red(line)}`);
|
|
11094
|
+
}
|
|
11095
|
+
}
|
|
11096
|
+
}
|
|
11097
|
+
}
|
|
11098
|
+
}
|
|
11099
|
+
lines.push("");
|
|
11100
|
+
}
|
|
11101
|
+
return lines.join("\n").trimEnd();
|
|
11102
|
+
}
|
|
11103
|
+
async function executeRollback(spaceId, ops, remote) {
|
|
11104
|
+
const client = getMapiClient();
|
|
11105
|
+
const spaceIdNum = Number(spaceId);
|
|
11106
|
+
let created = 0;
|
|
11107
|
+
let updated = 0;
|
|
11108
|
+
let deleted = 0;
|
|
11109
|
+
const folderOps = ops.filter((op) => op.type === "componentFolder");
|
|
11110
|
+
const componentOps = ops.filter((op) => op.type === "component");
|
|
11111
|
+
const datasourceOps = ops.filter((op) => op.type === "datasource");
|
|
11112
|
+
const folderUuidRemap = /* @__PURE__ */ new Map();
|
|
11113
|
+
for (const op of folderOps.filter((o) => o.action !== "delete")) {
|
|
11114
|
+
if (op.action === "create") {
|
|
11115
|
+
const oldUuid = op.payload.uuid;
|
|
11116
|
+
const payload = toComponentFolderCreate(stripApiFields(op.payload));
|
|
11117
|
+
try {
|
|
11118
|
+
const response = await client.componentFolders.create({
|
|
11119
|
+
path: { space_id: spaceIdNum },
|
|
11120
|
+
body: { component_group: payload },
|
|
11121
|
+
throwOnError: true
|
|
11122
|
+
});
|
|
11123
|
+
const remoteUuid = response.data?.component_group?.uuid;
|
|
11124
|
+
if (remoteUuid && typeof oldUuid === "string") {
|
|
11125
|
+
folderUuidRemap.set(oldUuid, remoteUuid);
|
|
11126
|
+
}
|
|
11127
|
+
created++;
|
|
11128
|
+
} catch (error) {
|
|
11129
|
+
handleAPIError("push_component_group", error, `Failed to create folder ${op.name}`);
|
|
11130
|
+
}
|
|
11131
|
+
} else if (op.action === "update") {
|
|
11132
|
+
const existing = remote.componentFolders.get(op.name);
|
|
11133
|
+
if (existing?.id) {
|
|
11134
|
+
const payload = toComponentFolderCreate(stripApiFields(op.payload));
|
|
11135
|
+
try {
|
|
11136
|
+
await client.componentFolders.update(existing.id, {
|
|
11137
|
+
path: { space_id: spaceIdNum },
|
|
11138
|
+
body: { component_group: payload },
|
|
11139
|
+
throwOnError: true
|
|
11140
|
+
});
|
|
11141
|
+
updated++;
|
|
11142
|
+
} catch (error) {
|
|
11143
|
+
handleAPIError("update_component_group", error, `Failed to update folder ${op.name}`);
|
|
11144
|
+
}
|
|
11145
|
+
}
|
|
11146
|
+
}
|
|
11147
|
+
}
|
|
11148
|
+
for (const op of componentOps) {
|
|
11149
|
+
const oldUuid = op.payload.component_group_uuid;
|
|
11150
|
+
if (typeof oldUuid !== "string") {
|
|
11151
|
+
continue;
|
|
11152
|
+
}
|
|
11153
|
+
const newUuid = folderUuidRemap.get(oldUuid);
|
|
11154
|
+
if (newUuid) {
|
|
11155
|
+
op.payload.component_group_uuid = newUuid;
|
|
11156
|
+
}
|
|
11157
|
+
}
|
|
11158
|
+
for (const op of componentOps.filter((o) => o.action !== "delete")) {
|
|
11159
|
+
if (op.action === "create") {
|
|
11160
|
+
const payload = toComponentCreate(stripApiFields(op.payload));
|
|
11161
|
+
try {
|
|
11162
|
+
await client.components.create({
|
|
11163
|
+
path: { space_id: spaceIdNum },
|
|
11164
|
+
body: { component: payload },
|
|
11165
|
+
throwOnError: true
|
|
11166
|
+
});
|
|
11167
|
+
created++;
|
|
11168
|
+
} catch (error) {
|
|
11169
|
+
handleAPIError("push_component", error, `Failed to create component ${op.name}`);
|
|
11170
|
+
}
|
|
11171
|
+
} else if (op.action === "update") {
|
|
11172
|
+
const existing = remote.components.get(op.name);
|
|
11173
|
+
if (existing?.id) {
|
|
11174
|
+
const payload = toComponentUpdate(stripApiFields(op.payload));
|
|
11175
|
+
try {
|
|
11176
|
+
await client.components.update(existing.id, {
|
|
11177
|
+
path: { space_id: spaceIdNum },
|
|
11178
|
+
body: { component: payload },
|
|
11179
|
+
throwOnError: true
|
|
11180
|
+
});
|
|
11181
|
+
updated++;
|
|
11182
|
+
} catch (error) {
|
|
11183
|
+
handleAPIError("update_component", error, `Failed to update component ${op.name}`);
|
|
11184
|
+
}
|
|
11185
|
+
}
|
|
11186
|
+
}
|
|
11187
|
+
}
|
|
11188
|
+
for (const op of datasourceOps.filter((o) => o.action !== "delete")) {
|
|
11189
|
+
if (op.action === "create") {
|
|
11190
|
+
const payload = toDatasourceCreate(stripApiFields(op.payload));
|
|
11191
|
+
try {
|
|
11192
|
+
await client.datasources.create({
|
|
11193
|
+
path: { space_id: spaceIdNum },
|
|
11194
|
+
body: { datasource: payload },
|
|
11195
|
+
throwOnError: true
|
|
11196
|
+
});
|
|
11197
|
+
created++;
|
|
11198
|
+
} catch (error) {
|
|
11199
|
+
handleAPIError("push_datasource", error, `Failed to create datasource ${op.name}`);
|
|
11200
|
+
}
|
|
11201
|
+
} else if (op.action === "update") {
|
|
11202
|
+
const existing = remote.datasources.get(op.name);
|
|
11203
|
+
if (existing?.id) {
|
|
11204
|
+
const payload = toDatasourceUpdate(stripApiFields(op.payload), existing);
|
|
11205
|
+
try {
|
|
11206
|
+
await client.datasources.update(existing.id, {
|
|
11207
|
+
path: { space_id: spaceIdNum },
|
|
11208
|
+
body: { datasource: payload },
|
|
11209
|
+
throwOnError: true
|
|
11210
|
+
});
|
|
11211
|
+
updated++;
|
|
11212
|
+
} catch (error) {
|
|
11213
|
+
handleAPIError("update_datasource", error, `Failed to update datasource ${op.name}`);
|
|
11214
|
+
}
|
|
11215
|
+
}
|
|
11216
|
+
}
|
|
11217
|
+
}
|
|
11218
|
+
for (const op of datasourceOps.filter((o) => o.action === "delete")) {
|
|
11219
|
+
const existing = remote.datasources.get(op.name);
|
|
11220
|
+
if (existing?.id) {
|
|
11221
|
+
try {
|
|
11222
|
+
await client.datasources.delete(existing.id, {
|
|
11223
|
+
path: { space_id: spaceIdNum },
|
|
11224
|
+
throwOnError: true
|
|
11225
|
+
});
|
|
11226
|
+
deleted++;
|
|
11227
|
+
} catch (error) {
|
|
11228
|
+
handleAPIError("delete_datasource", error, `Failed to delete datasource ${op.name}`);
|
|
11229
|
+
}
|
|
11230
|
+
}
|
|
11231
|
+
}
|
|
11232
|
+
for (const op of componentOps.filter((o) => o.action === "delete")) {
|
|
11233
|
+
const existing = remote.components.get(op.name);
|
|
11234
|
+
if (existing?.id) {
|
|
11235
|
+
try {
|
|
11236
|
+
await client.components.delete(existing.id, {
|
|
11237
|
+
path: { space_id: spaceIdNum },
|
|
11238
|
+
throwOnError: true
|
|
11239
|
+
});
|
|
11240
|
+
deleted++;
|
|
11241
|
+
} catch (error) {
|
|
11242
|
+
handleAPIError("push_component", error, `Failed to delete component ${op.name}`);
|
|
11243
|
+
}
|
|
11244
|
+
}
|
|
11245
|
+
}
|
|
11246
|
+
for (const op of folderOps.filter((o) => o.action === "delete")) {
|
|
11247
|
+
const existing = remote.componentFolders.get(op.name);
|
|
11248
|
+
if (existing?.id) {
|
|
11249
|
+
try {
|
|
11250
|
+
await client.componentFolders.delete(existing.id, {
|
|
11251
|
+
path: { space_id: spaceIdNum },
|
|
11252
|
+
throwOnError: true
|
|
11253
|
+
});
|
|
11254
|
+
deleted++;
|
|
11255
|
+
} catch (error) {
|
|
11256
|
+
handleAPIError("push_component_group", error, `Failed to delete folder ${op.name}`);
|
|
11257
|
+
}
|
|
11258
|
+
}
|
|
11259
|
+
}
|
|
11260
|
+
return { created, updated, deleted };
|
|
11261
|
+
}
|
|
11262
|
+
|
|
11263
|
+
schemaCommand.command("rollback [changeset-file]").description("Roll back a Storyblok space to the state captured in a changeset").option("-s, --space <space>", "space ID").option("-p, --path <path>", "path for file storage").option("--dry-run", "Show what would be undone without applying changes", false).option("--yes", "Skip confirmation prompt", false).option("--latest", "Automatically select the most recent changeset", false).action(async (changesetFile, options, command) => {
|
|
11264
|
+
const ui = getUI();
|
|
11265
|
+
const logger = getLogger();
|
|
11266
|
+
const reporter = getReporter();
|
|
11267
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
11268
|
+
const { state } = session();
|
|
11269
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Rolling back schema...");
|
|
11270
|
+
logger.info("Schema rollback started", { changesetFile, space });
|
|
11271
|
+
if (!requireAuthentication(state, verbose)) {
|
|
11272
|
+
return;
|
|
11273
|
+
}
|
|
11274
|
+
if (!space) {
|
|
11275
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
11276
|
+
return;
|
|
11277
|
+
}
|
|
11278
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
11279
|
+
try {
|
|
11280
|
+
const resolvedBase = resolvePath(basePath, "");
|
|
11281
|
+
let resolvedFile;
|
|
11282
|
+
if (changesetFile) {
|
|
11283
|
+
resolvedFile = changesetFile;
|
|
11284
|
+
} else if (options.latest) {
|
|
11285
|
+
const available = await listChangesets(resolvedBase);
|
|
11286
|
+
if (available.length === 0) {
|
|
11287
|
+
ui.warn("No changesets found. Run `schema push` first to create one.");
|
|
11288
|
+
return;
|
|
11289
|
+
}
|
|
11290
|
+
resolvedFile = available[0];
|
|
11291
|
+
ui.info(`Using latest changeset: ${basename(resolvedFile)}`);
|
|
11292
|
+
} else {
|
|
11293
|
+
const available = await listChangesets(resolvedBase);
|
|
11294
|
+
if (available.length === 0) {
|
|
11295
|
+
ui.warn("No changesets found. Run `schema push` first to create one.");
|
|
11296
|
+
return;
|
|
11297
|
+
}
|
|
11298
|
+
resolvedFile = await select({
|
|
11299
|
+
message: "Select a changeset to roll back:",
|
|
11300
|
+
choices: available.map((f) => ({ name: basename(f), value: f }))
|
|
11301
|
+
});
|
|
11302
|
+
}
|
|
11303
|
+
let changeset;
|
|
11304
|
+
try {
|
|
11305
|
+
changeset = await loadChangeset(resolvedFile);
|
|
11306
|
+
} catch (maybeError) {
|
|
11307
|
+
handleError(toError(maybeError), verbose);
|
|
11308
|
+
return;
|
|
11309
|
+
}
|
|
11310
|
+
logger.info("Changeset loaded", { file: resolvedFile, changes: changeset.changes.length });
|
|
11311
|
+
const ops = buildRollbackOps(changeset);
|
|
11312
|
+
if (ops.length === 0) {
|
|
11313
|
+
ui.ok("Changeset has no changes \u2014 nothing to roll back.");
|
|
11314
|
+
return;
|
|
11315
|
+
}
|
|
11316
|
+
ui.br();
|
|
11317
|
+
ui.log(formatRollbackOutput(changeset.changes));
|
|
11318
|
+
if (options.dryRun) {
|
|
11319
|
+
ui.info("Dry run \u2014 no changes applied.");
|
|
11320
|
+
logger.info("Dry run completed", { ops: ops.length });
|
|
11321
|
+
return;
|
|
11322
|
+
}
|
|
11323
|
+
if (!options.yes) {
|
|
11324
|
+
const confirmed = await confirm({
|
|
11325
|
+
message: `Apply rollback of ${ops.length} change(s) from ${basename(resolvedFile)}?`,
|
|
11326
|
+
default: false
|
|
11327
|
+
});
|
|
11328
|
+
if (!confirmed) {
|
|
11329
|
+
ui.info("Rollback cancelled.");
|
|
11330
|
+
return;
|
|
11331
|
+
}
|
|
11332
|
+
}
|
|
11333
|
+
const remoteSpinner = ui.createSpinner(`Fetching current remote state from space ${space}...`);
|
|
11334
|
+
let remoteResult;
|
|
11335
|
+
try {
|
|
11336
|
+
remoteResult = await fetchRemoteSchema(space);
|
|
11337
|
+
} catch (maybeError) {
|
|
11338
|
+
remoteSpinner.failed("Failed to fetch remote schema");
|
|
11339
|
+
handleError(toError(maybeError), verbose);
|
|
11340
|
+
return;
|
|
11341
|
+
}
|
|
11342
|
+
const { remote } = remoteResult;
|
|
11343
|
+
remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
|
|
11344
|
+
const rollbackSpinner = ui.createSpinner("Applying rollback...");
|
|
11345
|
+
let result;
|
|
11346
|
+
try {
|
|
11347
|
+
result = await executeRollback(space, ops, remote);
|
|
11348
|
+
} catch (error) {
|
|
11349
|
+
rollbackSpinner.failed("Failed to apply rollback");
|
|
11350
|
+
throw error;
|
|
11351
|
+
}
|
|
11352
|
+
summary.total = result.created + result.updated + result.deleted;
|
|
11353
|
+
summary.succeeded = summary.total;
|
|
11354
|
+
rollbackSpinner.succeed(`Rolled back: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted.`);
|
|
11355
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11356
|
+
const rollbackChangesetPath = await saveChangeset(resolvedBase, {
|
|
11357
|
+
timestamp,
|
|
11358
|
+
spaceId: Number(space),
|
|
11359
|
+
remote: { components: remoteResult.rawComponents, componentFolders: remoteResult.rawComponentFolders, datasources: remoteResult.rawDatasources },
|
|
11360
|
+
changes: ops.map((op) => ({
|
|
11361
|
+
type: op.type,
|
|
11362
|
+
name: op.name,
|
|
11363
|
+
action: op.action,
|
|
11364
|
+
...Object.keys(op.payload).length > 0 && { after: op.payload }
|
|
11365
|
+
}))
|
|
11366
|
+
});
|
|
11367
|
+
logger.info("Rollback changeset saved", { path: rollbackChangesetPath });
|
|
11368
|
+
} catch (maybeError) {
|
|
11369
|
+
summary.failed += 1;
|
|
11370
|
+
handleError(toError(maybeError), verbose);
|
|
11371
|
+
} finally {
|
|
11372
|
+
logger.info("Schema rollback finished", { summary });
|
|
11373
|
+
reporter.addSummary("schemaRollbackResults", summary);
|
|
11374
|
+
reporter.finalize();
|
|
11375
|
+
}
|
|
11376
|
+
});
|
|
11377
|
+
|
|
9394
11378
|
const program = getProgram();
|
|
9395
11379
|
konsola.br();
|
|
9396
11380
|
konsola.br();
|