storyblok 4.18.2 → 4.19.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 +2212 -153
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
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, readdir, readFile as readFile$1, appendFile, access, constants, 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",
|
|
@@ -1048,11 +1051,11 @@ function requireAuthentication(state, verbose = false) {
|
|
|
1048
1051
|
return true;
|
|
1049
1052
|
}
|
|
1050
1053
|
|
|
1051
|
-
const toCamelCase = (str) => {
|
|
1054
|
+
const toCamelCase$1 = (str) => {
|
|
1052
1055
|
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, "");
|
|
1053
1056
|
};
|
|
1054
1057
|
const toPascalCase = (str) => {
|
|
1055
|
-
const camelCase = toCamelCase(str);
|
|
1058
|
+
const camelCase = toCamelCase$1(str);
|
|
1056
1059
|
return camelCase ? camelCase[0].toUpperCase() + camelCase.slice(1) : camelCase;
|
|
1057
1060
|
};
|
|
1058
1061
|
const capitalize = (str) => {
|
|
@@ -1150,6 +1153,25 @@ function getPackageJson() {
|
|
|
1150
1153
|
return packageJson$1;
|
|
1151
1154
|
}
|
|
1152
1155
|
|
|
1156
|
+
async function fetchAllPages(fetchFunction, extractDataFunction) {
|
|
1157
|
+
const items = [];
|
|
1158
|
+
let page = 1;
|
|
1159
|
+
while (true) {
|
|
1160
|
+
const { data, response } = await fetchFunction(page);
|
|
1161
|
+
const totalHeader = response.headers.get("total");
|
|
1162
|
+
const fetchedItems = extractDataFunction(data);
|
|
1163
|
+
items.push(...fetchedItems);
|
|
1164
|
+
if (!totalHeader) {
|
|
1165
|
+
return items;
|
|
1166
|
+
}
|
|
1167
|
+
const total = Number(totalHeader);
|
|
1168
|
+
if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
|
|
1169
|
+
return items;
|
|
1170
|
+
}
|
|
1171
|
+
page++;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1153
1175
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
1154
1176
|
const __dirname$1 = dirname(__filename$1);
|
|
1155
1177
|
function isRegion(value) {
|
|
@@ -1238,6 +1260,10 @@ class UI {
|
|
|
1238
1260
|
this.br();
|
|
1239
1261
|
}
|
|
1240
1262
|
}
|
|
1263
|
+
/** Plain console.log passthrough — use for preformatted or multi-line text. */
|
|
1264
|
+
log(message) {
|
|
1265
|
+
this.console?.log(message);
|
|
1266
|
+
}
|
|
1241
1267
|
list(items) {
|
|
1242
1268
|
for (const item of items) {
|
|
1243
1269
|
this.console?.log(` ${item}`);
|
|
@@ -2147,14 +2173,14 @@ async function performInteractiveLogin(options) {
|
|
|
2147
2173
|
}
|
|
2148
2174
|
}
|
|
2149
2175
|
|
|
2150
|
-
const program$
|
|
2176
|
+
const program$f = getProgram();
|
|
2151
2177
|
const allRegionsText = Object.values(regions).join(",");
|
|
2152
|
-
program$
|
|
2178
|
+
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(
|
|
2153
2179
|
"-r, --region <region>",
|
|
2154
2180
|
`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}.`
|
|
2155
2181
|
).action(async (options) => {
|
|
2156
2182
|
konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
|
|
2157
|
-
const verbose = program$
|
|
2183
|
+
const verbose = program$f.opts().verbose;
|
|
2158
2184
|
const { token, region } = options;
|
|
2159
2185
|
const { state, updateSession, persistCredentials } = session();
|
|
2160
2186
|
if (state.isLoggedIn && !state.envLogin) {
|
|
@@ -2212,10 +2238,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
|
|
|
2212
2238
|
konsola.br();
|
|
2213
2239
|
});
|
|
2214
2240
|
|
|
2215
|
-
const program$
|
|
2216
|
-
program$
|
|
2241
|
+
const program$e = getProgram();
|
|
2242
|
+
program$e.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
|
|
2217
2243
|
konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
|
|
2218
|
-
const verbose = program$
|
|
2244
|
+
const verbose = program$e.opts().verbose;
|
|
2219
2245
|
try {
|
|
2220
2246
|
const { state } = session();
|
|
2221
2247
|
if (!state.isLoggedIn || !state.password || !state.region) {
|
|
@@ -2262,10 +2288,10 @@ async function openSignupInBrowser(url) {
|
|
|
2262
2288
|
}
|
|
2263
2289
|
}
|
|
2264
2290
|
|
|
2265
|
-
const program$
|
|
2266
|
-
program$
|
|
2291
|
+
const program$d = getProgram();
|
|
2292
|
+
program$d.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
|
|
2267
2293
|
konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
|
|
2268
|
-
const verbose = program$
|
|
2294
|
+
const verbose = program$d.opts().verbose;
|
|
2269
2295
|
const { state } = session();
|
|
2270
2296
|
if (state.isLoggedIn && !state.envLogin) {
|
|
2271
2297
|
konsola.ok(`You are already logged in. If you want to signup with a different account, please logout first.`);
|
|
@@ -2286,10 +2312,10 @@ program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
|
|
|
2286
2312
|
konsola.br();
|
|
2287
2313
|
});
|
|
2288
2314
|
|
|
2289
|
-
const program$
|
|
2290
|
-
program$
|
|
2315
|
+
const program$c = getProgram();
|
|
2316
|
+
program$c.command(commands.USER).description("Get the current user").action(async () => {
|
|
2291
2317
|
konsola.title(`${commands.USER}`, colorPalette.USER);
|
|
2292
|
-
const verbose = program$
|
|
2318
|
+
const verbose = program$c.opts().verbose;
|
|
2293
2319
|
const { state } = session();
|
|
2294
2320
|
if (!requireAuthentication(state)) {
|
|
2295
2321
|
return;
|
|
@@ -2317,10 +2343,10 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
|
|
|
2317
2343
|
konsola.br();
|
|
2318
2344
|
});
|
|
2319
2345
|
|
|
2320
|
-
const program$
|
|
2321
|
-
const componentsCommand = program$
|
|
2346
|
+
const program$b = getProgram();
|
|
2347
|
+
const componentsCommand = program$b.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`);
|
|
2322
2348
|
|
|
2323
|
-
function isComponent(item) {
|
|
2349
|
+
function isComponent$1(item) {
|
|
2324
2350
|
return "schema" in item;
|
|
2325
2351
|
}
|
|
2326
2352
|
function isPreset(item) {
|
|
@@ -2354,7 +2380,7 @@ async function loadComponents(directoryPath, options) {
|
|
|
2354
2380
|
throw new Error('Internal tag is missing "id"!');
|
|
2355
2381
|
}
|
|
2356
2382
|
tagMap.set(item.id, item);
|
|
2357
|
-
} else if (isComponent(item)) {
|
|
2383
|
+
} else if (isComponent$1(item)) {
|
|
2358
2384
|
const existing = componentMap.get(item.name);
|
|
2359
2385
|
if (existing) {
|
|
2360
2386
|
duplicates.push(`Component "${item.name}" found in both "${existing.file}" and "${file}"`);
|
|
@@ -2390,44 +2416,20 @@ To fix this, either:
|
|
|
2390
2416
|
}
|
|
2391
2417
|
|
|
2392
2418
|
const DEFAULT_COMPONENTS_FILENAME = "components";
|
|
2393
|
-
const DEFAULT_GROUPS_FILENAME = "groups";
|
|
2419
|
+
const DEFAULT_GROUPS_FILENAME$1 = "groups";
|
|
2394
2420
|
const DEFAULT_PRESETS_FILENAME = "presets";
|
|
2395
2421
|
const DEFAULT_TAGS_FILENAME = "tags";
|
|
2396
2422
|
|
|
2397
|
-
async function fetchAllPages(fetchFunction, extractDataFunction) {
|
|
2398
|
-
const items = [];
|
|
2399
|
-
let page = 1;
|
|
2400
|
-
while (true) {
|
|
2401
|
-
const { data, response } = await fetchFunction(page);
|
|
2402
|
-
const totalHeader = response.headers.get("total");
|
|
2403
|
-
const fetchedItems = extractDataFunction(data);
|
|
2404
|
-
items.push(...fetchedItems);
|
|
2405
|
-
if (!totalHeader) {
|
|
2406
|
-
return items;
|
|
2407
|
-
}
|
|
2408
|
-
const total = Number(totalHeader);
|
|
2409
|
-
if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
|
|
2410
|
-
return items;
|
|
2411
|
-
}
|
|
2412
|
-
page++;
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
2423
|
const fetchComponents = async (spaceId) => {
|
|
2417
2424
|
try {
|
|
2418
2425
|
const client = getMapiClient();
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
},
|
|
2427
|
-
throwOnError: true
|
|
2428
|
-
}),
|
|
2429
|
-
(data) => data?.components ?? []
|
|
2430
|
-
);
|
|
2426
|
+
const { data } = await client.components.list({
|
|
2427
|
+
path: {
|
|
2428
|
+
space_id: Number(spaceId)
|
|
2429
|
+
},
|
|
2430
|
+
throwOnError: true
|
|
2431
|
+
});
|
|
2432
|
+
return data?.components ?? [];
|
|
2431
2433
|
} catch (error) {
|
|
2432
2434
|
handleAPIError("pull_components", error);
|
|
2433
2435
|
}
|
|
@@ -2435,19 +2437,16 @@ const fetchComponents = async (spaceId) => {
|
|
|
2435
2437
|
const fetchComponent = async (spaceId, componentName) => {
|
|
2436
2438
|
try {
|
|
2437
2439
|
const client = getMapiClient();
|
|
2438
|
-
const
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
}),
|
|
2449
|
-
(data) => data?.components ?? []
|
|
2450
|
-
);
|
|
2440
|
+
const { data } = await client.components.list({
|
|
2441
|
+
path: {
|
|
2442
|
+
space_id: Number(spaceId)
|
|
2443
|
+
},
|
|
2444
|
+
query: {
|
|
2445
|
+
search: componentName
|
|
2446
|
+
},
|
|
2447
|
+
throwOnError: true
|
|
2448
|
+
});
|
|
2449
|
+
const matches = data?.components ?? [];
|
|
2451
2450
|
return matches.find((c) => c.name === componentName);
|
|
2452
2451
|
} catch (error) {
|
|
2453
2452
|
handleAPIError("pull_components", error, `Failed to fetch component ${componentName}`);
|
|
@@ -2514,7 +2513,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2514
2513
|
const presetsFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.${suffix}.json` : `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.json`);
|
|
2515
2514
|
await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2));
|
|
2516
2515
|
}
|
|
2517
|
-
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
|
|
2516
|
+
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
|
|
2518
2517
|
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
|
|
2519
2518
|
const internalTagsFilePath = join(resolvedPath, suffix ? `${DEFAULT_TAGS_FILENAME}.${suffix}.json` : `${DEFAULT_TAGS_FILENAME}.json`);
|
|
2520
2519
|
await saveToFile(internalTagsFilePath, JSON.stringify(internalTags, null, 2));
|
|
@@ -2524,7 +2523,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2524
2523
|
const componentsFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
|
|
2525
2524
|
await saveToFile(componentsFilePath, JSON.stringify(components, null, 2));
|
|
2526
2525
|
if (groups.length > 0) {
|
|
2527
|
-
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
|
|
2526
|
+
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
|
|
2528
2527
|
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
|
|
2529
2528
|
}
|
|
2530
2529
|
if (presets.length > 0) {
|
|
@@ -2540,6 +2539,27 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2540
2539
|
}
|
|
2541
2540
|
};
|
|
2542
2541
|
|
|
2542
|
+
function isSchemaField$1(value) {
|
|
2543
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
2544
|
+
}
|
|
2545
|
+
function toWritableSchema(schema) {
|
|
2546
|
+
if (!schema) {
|
|
2547
|
+
return void 0;
|
|
2548
|
+
}
|
|
2549
|
+
const result = {};
|
|
2550
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2551
|
+
if (isSchemaField$1(value)) {
|
|
2552
|
+
result[key] = value;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
return result;
|
|
2556
|
+
}
|
|
2557
|
+
function toRequestTagIds(tagIds) {
|
|
2558
|
+
if (!tagIds) {
|
|
2559
|
+
return void 0;
|
|
2560
|
+
}
|
|
2561
|
+
return tagIds.map((id) => Number(id));
|
|
2562
|
+
}
|
|
2543
2563
|
const pushComponent = async (space, component) => {
|
|
2544
2564
|
try {
|
|
2545
2565
|
const client = getMapiClient();
|
|
@@ -2574,10 +2594,23 @@ const updateComponent = async (space, componentId, component) => {
|
|
|
2574
2594
|
}
|
|
2575
2595
|
};
|
|
2576
2596
|
const upsertComponent = async (space, component, existingId) => {
|
|
2597
|
+
const { name, display_name, schema, is_root, is_nestable, component_group_uuid, color, icon, preview_field, internal_tag_ids } = component;
|
|
2598
|
+
const payload = {
|
|
2599
|
+
name,
|
|
2600
|
+
display_name: display_name ?? void 0,
|
|
2601
|
+
schema: toWritableSchema(schema),
|
|
2602
|
+
is_root,
|
|
2603
|
+
is_nestable,
|
|
2604
|
+
component_group_uuid: component_group_uuid ?? void 0,
|
|
2605
|
+
color: color ?? void 0,
|
|
2606
|
+
icon: icon ?? void 0,
|
|
2607
|
+
preview_field: preview_field ?? void 0,
|
|
2608
|
+
internal_tag_ids: toRequestTagIds(internal_tag_ids)
|
|
2609
|
+
};
|
|
2577
2610
|
if (existingId) {
|
|
2578
|
-
return await updateComponent(space, existingId,
|
|
2611
|
+
return await updateComponent(space, existingId, payload);
|
|
2579
2612
|
} else {
|
|
2580
|
-
return await pushComponent(space,
|
|
2613
|
+
return await pushComponent(space, payload);
|
|
2581
2614
|
}
|
|
2582
2615
|
};
|
|
2583
2616
|
const pushComponentGroup = async (space, componentGroup) => {
|
|
@@ -2629,7 +2662,14 @@ const pushComponentPreset = async (space, preset) => {
|
|
|
2629
2662
|
space_id: Number(space)
|
|
2630
2663
|
},
|
|
2631
2664
|
body: {
|
|
2632
|
-
preset
|
|
2665
|
+
preset: {
|
|
2666
|
+
...preset,
|
|
2667
|
+
preset: preset.preset ?? void 0,
|
|
2668
|
+
image: preset.image ?? void 0,
|
|
2669
|
+
color: preset.color ?? void 0,
|
|
2670
|
+
icon: preset.icon ?? void 0,
|
|
2671
|
+
description: preset.description ?? void 0
|
|
2672
|
+
}
|
|
2633
2673
|
},
|
|
2634
2674
|
throwOnError: true
|
|
2635
2675
|
});
|
|
@@ -2646,7 +2686,14 @@ const updateComponentPreset = async (space, presetId, preset) => {
|
|
|
2646
2686
|
space_id: Number(space)
|
|
2647
2687
|
},
|
|
2648
2688
|
body: {
|
|
2649
|
-
preset
|
|
2689
|
+
preset: {
|
|
2690
|
+
...preset,
|
|
2691
|
+
preset: preset.preset ?? void 0,
|
|
2692
|
+
image: preset.image ?? void 0,
|
|
2693
|
+
color: preset.color ?? void 0,
|
|
2694
|
+
icon: preset.icon ?? void 0,
|
|
2695
|
+
description: preset.description ?? void 0
|
|
2696
|
+
}
|
|
2650
2697
|
},
|
|
2651
2698
|
throwOnError: true
|
|
2652
2699
|
});
|
|
@@ -2680,7 +2727,7 @@ const pushComponentInternalTag = async (space, componentInternalTag) => {
|
|
|
2680
2727
|
path: {
|
|
2681
2728
|
space_id: Number(space)
|
|
2682
2729
|
},
|
|
2683
|
-
body: componentInternalTag,
|
|
2730
|
+
body: { internal_tag: componentInternalTag },
|
|
2684
2731
|
throwOnError: true
|
|
2685
2732
|
});
|
|
2686
2733
|
return data.internal_tag;
|
|
@@ -2695,7 +2742,7 @@ const updateComponentInternalTag = async (space, tagId, componentInternalTag) =>
|
|
|
2695
2742
|
path: {
|
|
2696
2743
|
space_id: Number(space)
|
|
2697
2744
|
},
|
|
2698
|
-
body: componentInternalTag,
|
|
2745
|
+
body: { internal_tag: componentInternalTag },
|
|
2699
2746
|
throwOnError: true
|
|
2700
2747
|
});
|
|
2701
2748
|
return data.internal_tag;
|
|
@@ -3951,16 +3998,14 @@ const fetchSpace = async (spaceId) => {
|
|
|
3951
3998
|
handleAPIError("pull_spaces", error, `Failed to fetch space ${spaceId}`);
|
|
3952
3999
|
}
|
|
3953
4000
|
};
|
|
3954
|
-
const createSpace = async (space) => {
|
|
4001
|
+
const createSpace = async (space, query) => {
|
|
3955
4002
|
try {
|
|
3956
|
-
const { in_org, assign_partner, ...spaceData } = space;
|
|
3957
4003
|
const client = getMapiClient();
|
|
3958
4004
|
const { data } = await client.spaces.create({
|
|
3959
4005
|
body: {
|
|
3960
|
-
space
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
}
|
|
4006
|
+
space
|
|
4007
|
+
},
|
|
4008
|
+
query
|
|
3964
4009
|
});
|
|
3965
4010
|
return data?.space;
|
|
3966
4011
|
} catch (error) {
|
|
@@ -3971,10 +4016,10 @@ const createSpace = async (space) => {
|
|
|
3971
4016
|
const fetchLanguages = async (spaceId) => {
|
|
3972
4017
|
try {
|
|
3973
4018
|
const space = await fetchSpace(spaceId);
|
|
3974
|
-
if (space?.default_lang_name
|
|
4019
|
+
if (space?.default_lang_name && space?.languages?.length) {
|
|
3975
4020
|
return {
|
|
3976
|
-
default_lang_name: space
|
|
3977
|
-
languages: space
|
|
4021
|
+
default_lang_name: space.default_lang_name,
|
|
4022
|
+
languages: space.languages
|
|
3978
4023
|
};
|
|
3979
4024
|
}
|
|
3980
4025
|
} catch (error) {
|
|
@@ -3994,8 +4039,8 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
|
|
|
3994
4039
|
}
|
|
3995
4040
|
};
|
|
3996
4041
|
|
|
3997
|
-
const program$
|
|
3998
|
-
const languagesCommand = program$
|
|
4042
|
+
const program$a = getProgram();
|
|
4043
|
+
const languagesCommand = program$a.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`);
|
|
3999
4044
|
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");
|
|
4000
4045
|
pullCmd$3.action(async (options, command) => {
|
|
4001
4046
|
konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
|
|
@@ -4041,8 +4086,8 @@ pullCmd$3.action(async (options, command) => {
|
|
|
4041
4086
|
konsola.br();
|
|
4042
4087
|
});
|
|
4043
4088
|
|
|
4044
|
-
const program$
|
|
4045
|
-
const migrationsCommand = program$
|
|
4089
|
+
const program$9 = getProgram();
|
|
4090
|
+
const migrationsCommand = program$9.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`);
|
|
4046
4091
|
|
|
4047
4092
|
const getMigrationTemplate = () => {
|
|
4048
4093
|
return `export default function (block) {
|
|
@@ -4146,7 +4191,7 @@ const fetchStories = async (spaceId, params) => {
|
|
|
4146
4191
|
const fetchStory = async (spaceId, storyId) => {
|
|
4147
4192
|
try {
|
|
4148
4193
|
const client = getMapiClient();
|
|
4149
|
-
const { data } = await client.stories.get(storyId, {
|
|
4194
|
+
const { data } = await client.stories.get(Number(storyId), {
|
|
4150
4195
|
path: {
|
|
4151
4196
|
space_id: Number(spaceId)
|
|
4152
4197
|
},
|
|
@@ -4191,9 +4236,11 @@ const updateStory = async (spaceId, storyId, payload) => {
|
|
|
4191
4236
|
...payload.story,
|
|
4192
4237
|
// StoryUpdate2 expects `parent_id?: number`; normalize null → undefined.
|
|
4193
4238
|
parent_id: payload.story.parent_id ?? void 0
|
|
4194
|
-
}
|
|
4195
|
-
|
|
4196
|
-
|
|
4239
|
+
}
|
|
4240
|
+
},
|
|
4241
|
+
query: {
|
|
4242
|
+
force_update: payload.force_update === "1",
|
|
4243
|
+
...payload.publish ? { publish: Boolean(payload.publish) } : {}
|
|
4197
4244
|
},
|
|
4198
4245
|
throwOnError: true
|
|
4199
4246
|
});
|
|
@@ -4290,6 +4337,35 @@ const prefetchTargetStoriesByKeys = async (spaceId, keys, options) => {
|
|
|
4290
4337
|
return result;
|
|
4291
4338
|
};
|
|
4292
4339
|
|
|
4340
|
+
function parseFilterQuery(input) {
|
|
4341
|
+
const trimmed = input.trim();
|
|
4342
|
+
if (!trimmed) {
|
|
4343
|
+
return {};
|
|
4344
|
+
}
|
|
4345
|
+
if (trimmed.startsWith("{")) {
|
|
4346
|
+
return JSON.parse(trimmed);
|
|
4347
|
+
}
|
|
4348
|
+
const result = {};
|
|
4349
|
+
for (const clause of trimmed.split("&")) {
|
|
4350
|
+
if (!clause) {
|
|
4351
|
+
continue;
|
|
4352
|
+
}
|
|
4353
|
+
const eq = clause.indexOf("=");
|
|
4354
|
+
if (eq === -1) {
|
|
4355
|
+
continue;
|
|
4356
|
+
}
|
|
4357
|
+
const path = clause.slice(0, eq);
|
|
4358
|
+
const value = clause.slice(eq + 1);
|
|
4359
|
+
const keys = [...path.matchAll(/\[([^\]]+)\]/g)].map((match) => match[1]);
|
|
4360
|
+
if (keys.length < 2) {
|
|
4361
|
+
continue;
|
|
4362
|
+
}
|
|
4363
|
+
const [field, operation] = keys;
|
|
4364
|
+
result[field] = { ...result[field], [operation]: value };
|
|
4365
|
+
}
|
|
4366
|
+
return result;
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4293
4369
|
const PIPELINE_BACKPRESSURE_MULTIPLIER = 2;
|
|
4294
4370
|
const DEFAULT_PIPELINE_BACKPRESSURE = 12;
|
|
4295
4371
|
function createPipelineBackpressureLock(limit) {
|
|
@@ -4313,16 +4389,13 @@ const ERROR_CODES = {
|
|
|
4313
4389
|
async function* storiesIterator(spaceId, params, onTotal) {
|
|
4314
4390
|
try {
|
|
4315
4391
|
let perPage = 500;
|
|
4316
|
-
const
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
transformedParams.contain_component = params.componentName;
|
|
4321
|
-
delete transformedParams.componentName;
|
|
4392
|
+
const { componentName, query, ...rest } = params ?? {};
|
|
4393
|
+
const transformedParams = { ...rest };
|
|
4394
|
+
if (componentName) {
|
|
4395
|
+
transformedParams.contain_component = componentName;
|
|
4322
4396
|
}
|
|
4323
|
-
if (
|
|
4324
|
-
transformedParams.filter_query =
|
|
4325
|
-
delete transformedParams.query;
|
|
4397
|
+
if (query) {
|
|
4398
|
+
transformedParams.filter_query = parseFilterQuery(query);
|
|
4326
4399
|
}
|
|
4327
4400
|
const result = await fetchStories(spaceId, {
|
|
4328
4401
|
...transformedParams,
|
|
@@ -4647,8 +4720,8 @@ class MigrationStream extends Transform {
|
|
|
4647
4720
|
id: story.id,
|
|
4648
4721
|
name: story.name || "",
|
|
4649
4722
|
content: story.content,
|
|
4650
|
-
published: story.published,
|
|
4651
|
-
unpublished_changes: story.unpublished_changes
|
|
4723
|
+
published: story.published ?? void 0,
|
|
4724
|
+
unpublished_changes: story.unpublished_changes ?? void 0
|
|
4652
4725
|
},
|
|
4653
4726
|
migrationTimestamp: this.timestamp,
|
|
4654
4727
|
migrationNames
|
|
@@ -4664,8 +4737,8 @@ class MigrationStream extends Transform {
|
|
|
4664
4737
|
storyId: story.id,
|
|
4665
4738
|
name: story.name,
|
|
4666
4739
|
content: storyContent,
|
|
4667
|
-
published: story.published,
|
|
4668
|
-
unpublished_changes: story.unpublished_changes
|
|
4740
|
+
published: story.published ?? void 0,
|
|
4741
|
+
unpublished_changes: story.unpublished_changes ?? void 0
|
|
4669
4742
|
};
|
|
4670
4743
|
} else if (processed && !contentChanged) {
|
|
4671
4744
|
this.results.skipped.push({
|
|
@@ -4807,7 +4880,6 @@ class UpdateStream extends Writable {
|
|
|
4807
4880
|
const payload = {
|
|
4808
4881
|
story: {
|
|
4809
4882
|
content,
|
|
4810
|
-
id: storyId,
|
|
4811
4883
|
name: storyName
|
|
4812
4884
|
},
|
|
4813
4885
|
force_update: "1"
|
|
@@ -5056,7 +5128,6 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
|
|
|
5056
5128
|
const payload = {
|
|
5057
5129
|
story: {
|
|
5058
5130
|
content: story.content,
|
|
5059
|
-
id: story.storyId,
|
|
5060
5131
|
name: story.name
|
|
5061
5132
|
},
|
|
5062
5133
|
force_update: "1"
|
|
@@ -5096,8 +5167,8 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
|
|
|
5096
5167
|
}
|
|
5097
5168
|
});
|
|
5098
5169
|
|
|
5099
|
-
const program$
|
|
5100
|
-
const typesCommand = program$
|
|
5170
|
+
const program$8 = getProgram();
|
|
5171
|
+
const typesCommand = program$8.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`);
|
|
5101
5172
|
|
|
5102
5173
|
const getAssetJSONSchema = (title) => ({
|
|
5103
5174
|
$id: "#/asset",
|
|
@@ -5708,7 +5779,7 @@ const getPropertyTypeAnnotation = (property, prefix, suffix) => {
|
|
|
5708
5779
|
}
|
|
5709
5780
|
};
|
|
5710
5781
|
function getStoryType(property, prefix, suffix) {
|
|
5711
|
-
return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase(property))}${suffix ?? ""}>`;
|
|
5782
|
+
return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase$1(property))}${suffix ?? ""}>`;
|
|
5712
5783
|
}
|
|
5713
5784
|
const getComponentType = (componentName, options) => {
|
|
5714
5785
|
const prefix = options.typePrefix ?? "";
|
|
@@ -6099,7 +6170,7 @@ const deleteDatasourceEntry = async (spaceId, entryId) => {
|
|
|
6099
6170
|
handleAPIError("delete_datasource_entry", error, `Failed to delete datasource entry ${entryId}`);
|
|
6100
6171
|
}
|
|
6101
6172
|
};
|
|
6102
|
-
function isDatasource(item) {
|
|
6173
|
+
function isDatasource$1(item) {
|
|
6103
6174
|
return typeof item === "object" && item !== null && "slug" in item && typeof item.slug === "string";
|
|
6104
6175
|
}
|
|
6105
6176
|
const readDatasourcesFiles = async (options) => {
|
|
@@ -6132,7 +6203,7 @@ const readDatasourcesFiles = async (options) => {
|
|
|
6132
6203
|
continue;
|
|
6133
6204
|
}
|
|
6134
6205
|
for (const item of data) {
|
|
6135
|
-
if (isDatasource(item)) {
|
|
6206
|
+
if (isDatasource$1(item)) {
|
|
6136
6207
|
const existing = datasourceMap.get(item.slug);
|
|
6137
6208
|
if (existing) {
|
|
6138
6209
|
duplicates.push(`Datasource "${item.slug}" found in both "${existing.file}" and "${file}"`);
|
|
@@ -6232,8 +6303,8 @@ generateCmd.action(async (options, command) => {
|
|
|
6232
6303
|
}
|
|
6233
6304
|
});
|
|
6234
6305
|
|
|
6235
|
-
const program$
|
|
6236
|
-
const datasourcesCommand = program$
|
|
6306
|
+
const program$7 = getProgram();
|
|
6307
|
+
const datasourcesCommand = program$7.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
|
|
6237
6308
|
|
|
6238
6309
|
const DEFAULT_DATASOURCES_FILENAME = "datasources";
|
|
6239
6310
|
|
|
@@ -6844,13 +6915,13 @@ async function promptForLogin(verbose) {
|
|
|
6844
6915
|
return null;
|
|
6845
6916
|
}
|
|
6846
6917
|
}
|
|
6847
|
-
const program$
|
|
6848
|
-
program$
|
|
6918
|
+
const program$6 = getProgram();
|
|
6919
|
+
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(
|
|
6849
6920
|
"-r, --region <region>",
|
|
6850
6921
|
`The region to apply to the generated project template (does not affect space creation).`
|
|
6851
6922
|
).action(async (projectPath, options) => {
|
|
6852
6923
|
ui.title(`${commands.CREATE}`, colorPalette.CREATE);
|
|
6853
|
-
const verbose = program$
|
|
6924
|
+
const verbose = program$6.opts().verbose;
|
|
6854
6925
|
const { template, blueprint, token } = options;
|
|
6855
6926
|
if (options.region && !isRegion(options.region)) {
|
|
6856
6927
|
handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
|
|
@@ -7023,13 +7094,13 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
7023
7094
|
name: toHumanReadable(projectName),
|
|
7024
7095
|
domain: blueprintDomain
|
|
7025
7096
|
};
|
|
7097
|
+
const createSpaceQuery = {};
|
|
7026
7098
|
if (whereToCreateSpace === "org") {
|
|
7027
|
-
|
|
7028
|
-
spaceToCreate.in_org = true;
|
|
7099
|
+
createSpaceQuery.in_org = true;
|
|
7029
7100
|
} else if (whereToCreateSpace === "partner") {
|
|
7030
|
-
|
|
7101
|
+
createSpaceQuery.assign_partner = true;
|
|
7031
7102
|
}
|
|
7032
|
-
createdSpace = await createSpace(spaceToCreate);
|
|
7103
|
+
createdSpace = await createSpace(spaceToCreate, createSpaceQuery);
|
|
7033
7104
|
spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`);
|
|
7034
7105
|
if (createdSpace?.first_token) {
|
|
7035
7106
|
await handleEnvFileCreation(resolvedPath, createdSpace.first_token, region);
|
|
@@ -7069,8 +7140,8 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
7069
7140
|
ui.br();
|
|
7070
7141
|
});
|
|
7071
7142
|
|
|
7072
|
-
const program$
|
|
7073
|
-
const logsCommand = program$
|
|
7143
|
+
const program$5 = getProgram();
|
|
7144
|
+
const logsCommand = program$5.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`);
|
|
7074
7145
|
|
|
7075
7146
|
const listCmd$1 = logsCommand.command("list").description("List logs").option("-s, --space <space>", "space ID");
|
|
7076
7147
|
listCmd$1.action(async (_options, command) => {
|
|
@@ -7095,8 +7166,8 @@ pruneCmd$1.action(async (options, command) => {
|
|
|
7095
7166
|
ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
7096
7167
|
});
|
|
7097
7168
|
|
|
7098
|
-
const program$
|
|
7099
|
-
const reportsCommand = program$
|
|
7169
|
+
const program$4 = getProgram();
|
|
7170
|
+
const reportsCommand = program$4.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.");
|
|
7100
7171
|
|
|
7101
7172
|
const listCmd = reportsCommand.command("list").description("List reports").option("-s, --space <space>", "space ID");
|
|
7102
7173
|
listCmd.action(async (_options, command) => {
|
|
@@ -7121,8 +7192,8 @@ pruneCmd.action(async (options, command) => {
|
|
|
7121
7192
|
ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
7122
7193
|
});
|
|
7123
7194
|
|
|
7124
|
-
const program$
|
|
7125
|
-
const assetsCommand = program$
|
|
7195
|
+
const program$3 = getProgram();
|
|
7196
|
+
const assetsCommand = program$3.command(commands.ASSETS).description(`Manage your space's assets`);
|
|
7126
7197
|
|
|
7127
7198
|
const fetchAssets = async ({ spaceId, params }) => {
|
|
7128
7199
|
try {
|
|
@@ -7170,7 +7241,7 @@ const createAssetInternalTag = async (spaceId, name) => {
|
|
|
7170
7241
|
const client = getMapiClient();
|
|
7171
7242
|
const { data } = await client.internalTags.create({
|
|
7172
7243
|
path: { space_id: Number(spaceId) },
|
|
7173
|
-
body: { name, object_type: "asset" },
|
|
7244
|
+
body: { internal_tag: { name, object_type: "asset" } },
|
|
7174
7245
|
throwOnError: true
|
|
7175
7246
|
});
|
|
7176
7247
|
const tag = data?.internal_tag;
|
|
@@ -7294,7 +7365,7 @@ const updateAsset = async (id, asset, { spaceId, fileBuffer }) => {
|
|
|
7294
7365
|
const createAsset = async (asset, fileBuffer, { spaceId }) => {
|
|
7295
7366
|
try {
|
|
7296
7367
|
const client = getMapiClient();
|
|
7297
|
-
const { id: _id, ...assetBody } = asset;
|
|
7368
|
+
const { id: _id, filename: _filename, ...assetBody } = asset;
|
|
7298
7369
|
return await client.assets.create({
|
|
7299
7370
|
body: assetBody,
|
|
7300
7371
|
file: fileBuffer,
|
|
@@ -7354,6 +7425,26 @@ const parseAssetData = (raw) => {
|
|
|
7354
7425
|
throw new Error(`Invalid --data JSON: ${toError(maybeError).message}`);
|
|
7355
7426
|
}
|
|
7356
7427
|
};
|
|
7428
|
+
const toAssetUpload = (partial, shortFilename) => {
|
|
7429
|
+
const nullToUndef = (value) => value ?? void 0;
|
|
7430
|
+
return {
|
|
7431
|
+
id: partial.id,
|
|
7432
|
+
filename: partial.filename,
|
|
7433
|
+
short_filename: shortFilename,
|
|
7434
|
+
asset_folder_id: nullToUndef(partial.asset_folder_id),
|
|
7435
|
+
ext_id: nullToUndef(partial.ext_id),
|
|
7436
|
+
alt: nullToUndef(partial.alt),
|
|
7437
|
+
copyright: nullToUndef(partial.copyright),
|
|
7438
|
+
title: nullToUndef(partial.title),
|
|
7439
|
+
source: nullToUndef(partial.source),
|
|
7440
|
+
expire_at: nullToUndef(partial.expire_at),
|
|
7441
|
+
publish_at: nullToUndef(partial.publish_at),
|
|
7442
|
+
focus: nullToUndef(partial.focus),
|
|
7443
|
+
is_private: nullToUndef(partial.is_private),
|
|
7444
|
+
internal_tag_ids: partial.internal_tag_ids,
|
|
7445
|
+
meta_data: nullToUndef(partial.meta_data)
|
|
7446
|
+
};
|
|
7447
|
+
};
|
|
7357
7448
|
const getSidecarFilename = (assetBinaryPath) => {
|
|
7358
7449
|
return join(dirname(assetBinaryPath), `${basename(assetBinaryPath, extname(assetBinaryPath))}.json`);
|
|
7359
7450
|
};
|
|
@@ -7382,7 +7473,7 @@ const isRemoteSource = (assetBinaryPath) => {
|
|
|
7382
7473
|
return false;
|
|
7383
7474
|
}
|
|
7384
7475
|
};
|
|
7385
|
-
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.
|
|
7476
|
+
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.new_filename);
|
|
7386
7477
|
const loadAssetMap = async (manifestFile) => {
|
|
7387
7478
|
const manifest = await loadManifest(manifestFile);
|
|
7388
7479
|
const entries = manifest.filter(isValidManifestEntry).map((e) => [
|
|
@@ -7762,8 +7853,10 @@ const readLocalAssetsStream = ({
|
|
|
7762
7853
|
const sidecar = await loadSidecarAssetData(binaryFilePath);
|
|
7763
7854
|
const shortFilename = sidecar.short_filename || (sidecar.filename ? basename(sidecar.filename) : void 0) || file;
|
|
7764
7855
|
const asset = {
|
|
7765
|
-
...sidecar,
|
|
7766
|
-
|
|
7856
|
+
...toAssetUpload(sidecar, shortFilename),
|
|
7857
|
+
// Carry the read-only tag detail for source→target tag-name
|
|
7858
|
+
// translation in `processAsset`; it is stripped before the API call.
|
|
7859
|
+
...sidecar.internal_tags_list ? { internal_tags_list: sidecar.internal_tags_list } : {}
|
|
7767
7860
|
};
|
|
7768
7861
|
const fileBuffer = await readFile$1(binaryFilePath);
|
|
7769
7862
|
const sidecarPath = getSidecarFilename(binaryFilePath);
|
|
@@ -7847,7 +7940,7 @@ const mapInternalTagIds = (sourceIds, sourceTags, assetInternalTagsByName, onUnm
|
|
|
7847
7940
|
const sourceName = sourceNamesById.get(sourceId);
|
|
7848
7941
|
const targetId = typeof sourceName === "string" ? assetInternalTagsByName.get(sourceName) : void 0;
|
|
7849
7942
|
if (typeof targetId === "number") {
|
|
7850
|
-
mapped.push(
|
|
7943
|
+
mapped.push(targetId);
|
|
7851
7944
|
} else {
|
|
7852
7945
|
onUnmappedTag?.({ sourceId, name: sourceName });
|
|
7853
7946
|
}
|
|
@@ -7899,10 +7992,11 @@ const processAsset = async ({
|
|
|
7899
7992
|
const remoteAssetId = hasId(localAsset) ? maps.assets.get(localAsset.id)?.new.id || localAsset.id : void 0;
|
|
7900
7993
|
const remoteAsset = remoteAssetId ? await transports.getAsset(remoteAssetId) : null;
|
|
7901
7994
|
const sourceTags = localAsset.internal_tags_list;
|
|
7902
|
-
const resolveInternalTagIds = (sourceIds) => maps.assetInternalTagsByName ? mapInternalTagIds(sourceIds, sourceTags, maps.assetInternalTagsByName, onUnmappedTag) : (sourceIds ?? []).map((id) =>
|
|
7995
|
+
const resolveInternalTagIds = (sourceIds) => maps.assetInternalTagsByName ? mapInternalTagIds(sourceIds, sourceTags, maps.assetInternalTagsByName, onUnmappedTag) : (sourceIds ?? []).map((id) => Number(id));
|
|
7903
7996
|
let newRemoteAsset;
|
|
7904
7997
|
let status;
|
|
7905
7998
|
if (remoteAsset) {
|
|
7999
|
+
const nullToUndef = (v) => v ?? void 0;
|
|
7906
8000
|
const updatePayload = {
|
|
7907
8001
|
asset_folder_id: remoteFolderId,
|
|
7908
8002
|
alt: "alt" in localAsset ? localAsset.alt : remoteAsset.alt,
|
|
@@ -7911,24 +8005,28 @@ const processAsset = async ({
|
|
|
7911
8005
|
source: "source" in localAsset ? localAsset.source : remoteAsset.source,
|
|
7912
8006
|
is_private: "is_private" in localAsset ? localAsset.is_private : remoteAsset.is_private,
|
|
7913
8007
|
focus: "focus" in localAsset ? localAsset.focus : remoteAsset.focus,
|
|
7914
|
-
expire_at: "expire_at" in localAsset ? localAsset.expire_at : remoteAsset.expire_at,
|
|
7915
|
-
publish_at: "publish_at" in localAsset ? localAsset.publish_at : remoteAsset.publish_at,
|
|
7916
|
-
internal_tag_ids:
|
|
7917
|
-
meta_data: "meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data
|
|
8008
|
+
expire_at: nullToUndef("expire_at" in localAsset ? localAsset.expire_at : remoteAsset.expire_at),
|
|
8009
|
+
publish_at: nullToUndef("publish_at" in localAsset ? localAsset.publish_at : remoteAsset.publish_at),
|
|
8010
|
+
internal_tag_ids: localAsset.internal_tag_ids ? resolveInternalTagIds(localAsset.internal_tag_ids) : remoteAsset.internal_tag_ids,
|
|
8011
|
+
meta_data: nullToUndef("meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data)
|
|
7918
8012
|
};
|
|
7919
8013
|
await transports.updateAsset(
|
|
7920
8014
|
remoteAsset.id,
|
|
7921
8015
|
{ ...updatePayload, short_filename: remoteAsset.short_filename },
|
|
7922
8016
|
fileBuffer
|
|
7923
8017
|
);
|
|
7924
|
-
newRemoteAsset = {
|
|
8018
|
+
newRemoteAsset = {
|
|
8019
|
+
...remoteAsset,
|
|
8020
|
+
...updatePayload,
|
|
8021
|
+
is_private: updatePayload.is_private ?? remoteAsset.is_private
|
|
8022
|
+
};
|
|
7925
8023
|
status = "updated";
|
|
7926
8024
|
} else if (hasShortFilename(localAsset)) {
|
|
7927
|
-
const
|
|
7928
|
-
const
|
|
8025
|
+
const mappedTagIds = localAsset.internal_tag_ids ? resolveInternalTagIds(localAsset.internal_tag_ids) : void 0;
|
|
8026
|
+
const { internal_tag_ids: _sourceTagIds, ...uploadBase } = toAssetUpload(localAsset, localAsset.short_filename);
|
|
7929
8027
|
const createPayload = {
|
|
7930
|
-
...
|
|
7931
|
-
asset_folder_id: remoteFolderId,
|
|
8028
|
+
...uploadBase,
|
|
8029
|
+
asset_folder_id: remoteFolderId ?? void 0,
|
|
7932
8030
|
...mappedTagIds !== void 0 ? { internal_tag_ids: mappedTagIds } : {}
|
|
7933
8031
|
};
|
|
7934
8032
|
newRemoteAsset = await transports.createAsset(createPayload, fileBuffer);
|
|
@@ -8161,8 +8259,8 @@ const traverseAndMapBySchema = (data, {
|
|
|
8161
8259
|
const dataNew = { ...data };
|
|
8162
8260
|
for (const [fieldName, fieldValue] of Object.entries(data)) {
|
|
8163
8261
|
const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
|
|
8164
|
-
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema
|
|
8165
|
-
const fieldRefMapper = typeof fieldType === "string"
|
|
8262
|
+
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema ? fieldSchema.type : void 0;
|
|
8263
|
+
const fieldRefMapper = typeof fieldType === "string" ? fieldRefMappers2[fieldType] : void 0;
|
|
8166
8264
|
if (fieldRefMapper) {
|
|
8167
8265
|
dataNew[fieldName] = fieldRefMapper(fieldValue, {
|
|
8168
8266
|
schema: fieldSchema,
|
|
@@ -8227,6 +8325,9 @@ const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRef
|
|
|
8227
8325
|
fieldRefMappers: fieldRefMappers2
|
|
8228
8326
|
});
|
|
8229
8327
|
const multilinkFieldRefMapper = (data, { maps }) => {
|
|
8328
|
+
if (!data || typeof data !== "object") {
|
|
8329
|
+
return data;
|
|
8330
|
+
}
|
|
8230
8331
|
if (data.linktype !== "story") {
|
|
8231
8332
|
return data;
|
|
8232
8333
|
}
|
|
@@ -8246,6 +8347,9 @@ const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMap
|
|
|
8246
8347
|
}));
|
|
8247
8348
|
};
|
|
8248
8349
|
const assetFieldRefMapper = (data, { maps }) => {
|
|
8350
|
+
if (!data || typeof data !== "object") {
|
|
8351
|
+
return data;
|
|
8352
|
+
}
|
|
8249
8353
|
const mappedAsset = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
|
|
8250
8354
|
if (!mappedAsset) {
|
|
8251
8355
|
return data;
|
|
@@ -8263,7 +8367,7 @@ const multiassetFieldRefMapper = (data, options) => {
|
|
|
8263
8367
|
return data.map((d) => assetFieldRefMapper(d, options));
|
|
8264
8368
|
};
|
|
8265
8369
|
const optionsFieldRefMapper = (data, { schema, maps }) => {
|
|
8266
|
-
if (schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
8370
|
+
if (!schema || !("source" in schema) || schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
8267
8371
|
return data;
|
|
8268
8372
|
}
|
|
8269
8373
|
return data.map((d) => maps.stories?.get(d) || d);
|
|
@@ -9112,11 +9216,7 @@ pushCmd$1.action(async (assetInput, options, command) => {
|
|
|
9112
9216
|
const sourceBasename = isRemoteSource(assetBinaryPath) ? basename(new URL(assetBinaryPath).pathname) : basename(assetBinaryPath);
|
|
9113
9217
|
const shortFilename = options.shortFilename || assetDataPartial.short_filename || sourceBasename;
|
|
9114
9218
|
const folderId = options.folder ? Number(options.folder) : void 0;
|
|
9115
|
-
assetData = {
|
|
9116
|
-
...assetDataPartial,
|
|
9117
|
-
short_filename: shortFilename,
|
|
9118
|
-
asset_folder_id: folderId
|
|
9119
|
-
};
|
|
9219
|
+
assetData = { ...toAssetUpload(assetDataPartial, shortFilename), asset_folder_id: folderId };
|
|
9120
9220
|
}
|
|
9121
9221
|
const getAssetTransport = makeGetAssetAPITransport({ spaceId: targetSpace });
|
|
9122
9222
|
const createAssetTransport = options.dryRun ? async (asset) => asset : makeCreateAssetAPITransport({ spaceId: targetSpace });
|
|
@@ -9276,8 +9376,8 @@ transferCmd.action(async (assetIds, options, command) => {
|
|
|
9276
9376
|
process.exitCode = summary.failed > 0 ? 1 : 0;
|
|
9277
9377
|
});
|
|
9278
9378
|
|
|
9279
|
-
const program$
|
|
9280
|
-
const storiesCommand = program$
|
|
9379
|
+
const program$2 = getProgram();
|
|
9380
|
+
const storiesCommand = program$2.command(commands.STORIES).description(`Manage your space's stories`);
|
|
9281
9381
|
|
|
9282
9382
|
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.`);
|
|
9283
9383
|
pullCmd.action(async (options, command) => {
|
|
@@ -9313,7 +9413,7 @@ pullCmd.action(async (options, command) => {
|
|
|
9313
9413
|
fetchStoriesStream({
|
|
9314
9414
|
spaceId: space,
|
|
9315
9415
|
params: {
|
|
9316
|
-
filter_query: options.query,
|
|
9416
|
+
filter_query: options.query ? parseFilterQuery(options.query) : void 0,
|
|
9317
9417
|
starts_with: options.startsWith
|
|
9318
9418
|
},
|
|
9319
9419
|
setTotalPages: (totalPages) => {
|
|
@@ -9782,6 +9882,1965 @@ pushCmd.action(async (options, command) => {
|
|
|
9782
9882
|
}
|
|
9783
9883
|
});
|
|
9784
9884
|
|
|
9885
|
+
const program$1 = getProgram();
|
|
9886
|
+
const schemaCommand = program$1.command(commands.SCHEMA).description(`Manage your space's schema from code`);
|
|
9887
|
+
|
|
9888
|
+
const COMPONENT_STRIP_KEYS = /* @__PURE__ */ new Set([
|
|
9889
|
+
"id",
|
|
9890
|
+
"created_at",
|
|
9891
|
+
"updated_at",
|
|
9892
|
+
"real_name",
|
|
9893
|
+
// API-computed display/technical name, read-only
|
|
9894
|
+
"preset_id",
|
|
9895
|
+
// Instance-level preset selection, not part of schema definition
|
|
9896
|
+
"all_presets",
|
|
9897
|
+
// Computed list of presets, managed via /presets API
|
|
9898
|
+
"internal_tags_list",
|
|
9899
|
+
// Read-only expanded form of internal_tag_ids ({id, name} objects)
|
|
9900
|
+
"content_type_asset_preview",
|
|
9901
|
+
// Read-only, not in ComponentCreate/ComponentUpdate
|
|
9902
|
+
"image",
|
|
9903
|
+
// Read-only preview image URL
|
|
9904
|
+
"preview_tmpl",
|
|
9905
|
+
// Read-only preview template
|
|
9906
|
+
"metadata"
|
|
9907
|
+
// Not in current API types, stripped defensively
|
|
9908
|
+
]);
|
|
9909
|
+
const DATASOURCE_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
|
|
9910
|
+
const DATASOURCE_DIMENSION_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "datasource_id", "created_at", "updated_at"]);
|
|
9911
|
+
const FOLDER_INIT_STRIP_KEYS = /* @__PURE__ */ new Set(["id"]);
|
|
9912
|
+
const FOLDER_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "uuid"]);
|
|
9913
|
+
const DATASOURCE_DEFAULTS = {
|
|
9914
|
+
dimensions: []
|
|
9915
|
+
};
|
|
9916
|
+
const COMPONENT_DEFAULTS = {
|
|
9917
|
+
display_name: "",
|
|
9918
|
+
description: "",
|
|
9919
|
+
color: "",
|
|
9920
|
+
icon: "",
|
|
9921
|
+
preview_field: "",
|
|
9922
|
+
internal_tag_ids: []
|
|
9923
|
+
};
|
|
9924
|
+
function applyDefaults(entity, defaults) {
|
|
9925
|
+
const result = { ...entity };
|
|
9926
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
9927
|
+
if (result[key] === void 0 || result[key] === null) {
|
|
9928
|
+
Object.assign(result, { [key]: defaultValue });
|
|
9929
|
+
}
|
|
9930
|
+
}
|
|
9931
|
+
return result;
|
|
9932
|
+
}
|
|
9933
|
+
const INDENT = " ";
|
|
9934
|
+
function formatValue(value, depth) {
|
|
9935
|
+
const indent = INDENT.repeat(depth);
|
|
9936
|
+
const innerIndent = INDENT.repeat(depth + 1);
|
|
9937
|
+
if (value === null || value === void 0) {
|
|
9938
|
+
return String(value);
|
|
9939
|
+
}
|
|
9940
|
+
if (typeof value === "string") {
|
|
9941
|
+
return `'${value.replace(/'/g, "\\'")}'`;
|
|
9942
|
+
}
|
|
9943
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
9944
|
+
return String(value);
|
|
9945
|
+
}
|
|
9946
|
+
if (Array.isArray(value)) {
|
|
9947
|
+
if (value.length === 0) {
|
|
9948
|
+
return "[]";
|
|
9949
|
+
}
|
|
9950
|
+
const items = value.map((item) => `${innerIndent}${formatValue(item, depth + 1)},`);
|
|
9951
|
+
return `[
|
|
9952
|
+
${items.join("\n")}
|
|
9953
|
+
${indent}]`;
|
|
9954
|
+
}
|
|
9955
|
+
if (typeof value === "object") {
|
|
9956
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0 && v !== null).sort(([a], [b]) => a.localeCompare(b));
|
|
9957
|
+
if (entries.length === 0) {
|
|
9958
|
+
return "{}";
|
|
9959
|
+
}
|
|
9960
|
+
const props = entries.map(
|
|
9961
|
+
([key, val]) => `${innerIndent}${key}: ${formatValue(val, depth + 1)},`
|
|
9962
|
+
);
|
|
9963
|
+
return `{
|
|
9964
|
+
${props.join("\n")}
|
|
9965
|
+
${indent}}`;
|
|
9966
|
+
}
|
|
9967
|
+
return String(value);
|
|
9968
|
+
}
|
|
9969
|
+
function fileTimestamp(iso) {
|
|
9970
|
+
return iso.replace(/\D/g, "").slice(0, 14);
|
|
9971
|
+
}
|
|
9972
|
+
function displayPath(filePath, userPath) {
|
|
9973
|
+
return userPath && isAbsolute(userPath) ? filePath : relative(process.cwd(), filePath);
|
|
9974
|
+
}
|
|
9975
|
+
function stripKeys(obj, keysToStrip) {
|
|
9976
|
+
const result = {};
|
|
9977
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
9978
|
+
if (!keysToStrip.has(key) && value !== void 0 && value !== null) {
|
|
9979
|
+
result[key] = value;
|
|
9980
|
+
}
|
|
9981
|
+
}
|
|
9982
|
+
return result;
|
|
9983
|
+
}
|
|
9984
|
+
|
|
9985
|
+
function isObject(value) {
|
|
9986
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9987
|
+
}
|
|
9988
|
+
function isComponent(value) {
|
|
9989
|
+
return isObject(value) && typeof value.name === "string" && "schema" in value && isObject(value.schema);
|
|
9990
|
+
}
|
|
9991
|
+
function isDatasource(value) {
|
|
9992
|
+
return isObject(value) && typeof value.name === "string" && typeof value.slug === "string" && !("schema" in value);
|
|
9993
|
+
}
|
|
9994
|
+
function isComponentFolder(value) {
|
|
9995
|
+
return isObject(value) && typeof value.name === "string" && !("schema" in value) && !("slug" in value) && ("uuid" in value || "parent_id" in value);
|
|
9996
|
+
}
|
|
9997
|
+
function isSchemaObject(value) {
|
|
9998
|
+
return isObject(value) && ("blocks" in value || "blockFolders" in value || "datasources" in value);
|
|
9999
|
+
}
|
|
10000
|
+
function classifyExports(moduleExports) {
|
|
10001
|
+
const components = [];
|
|
10002
|
+
const componentFolders = [];
|
|
10003
|
+
const datasources = [];
|
|
10004
|
+
function collect(value) {
|
|
10005
|
+
if (isComponent(value)) {
|
|
10006
|
+
components.push(value);
|
|
10007
|
+
} else if (isDatasource(value)) {
|
|
10008
|
+
datasources.push(value);
|
|
10009
|
+
} else if (isComponentFolder(value)) {
|
|
10010
|
+
componentFolders.push(value);
|
|
10011
|
+
}
|
|
10012
|
+
}
|
|
10013
|
+
for (const value of Object.values(moduleExports)) {
|
|
10014
|
+
if (isSchemaObject(value)) {
|
|
10015
|
+
for (const group of Object.values(value)) {
|
|
10016
|
+
if (isObject(group)) {
|
|
10017
|
+
for (const entity of Object.values(group)) {
|
|
10018
|
+
collect(entity);
|
|
10019
|
+
}
|
|
10020
|
+
}
|
|
10021
|
+
}
|
|
10022
|
+
} else {
|
|
10023
|
+
collect(value);
|
|
10024
|
+
}
|
|
10025
|
+
}
|
|
10026
|
+
return { components, componentFolders, datasources };
|
|
10027
|
+
}
|
|
10028
|
+
async function loadSchema(entryPath) {
|
|
10029
|
+
const { createJiti } = await import('jiti');
|
|
10030
|
+
const jiti = createJiti(import.meta.url, {
|
|
10031
|
+
interopDefault: true
|
|
10032
|
+
});
|
|
10033
|
+
const absolutePath = (await import('pathe')).resolve(entryPath);
|
|
10034
|
+
const mod = await jiti.import(absolutePath);
|
|
10035
|
+
return classifyExports(mod);
|
|
10036
|
+
}
|
|
10037
|
+
|
|
10038
|
+
function sortSchemaByPos$1(schema) {
|
|
10039
|
+
const entries = Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
|
|
10040
|
+
const posA = typeof a.pos === "number" ? a.pos : Infinity;
|
|
10041
|
+
const posB = typeof b.pos === "number" ? b.pos : Infinity;
|
|
10042
|
+
return posA - posB;
|
|
10043
|
+
});
|
|
10044
|
+
return Object.fromEntries(
|
|
10045
|
+
entries.map(([key, field]) => {
|
|
10046
|
+
const { id, ...rest } = field;
|
|
10047
|
+
return [key, rest];
|
|
10048
|
+
})
|
|
10049
|
+
);
|
|
10050
|
+
}
|
|
10051
|
+
function serializeComponent(component) {
|
|
10052
|
+
const clean = stripKeys(component, COMPONENT_STRIP_KEYS);
|
|
10053
|
+
if (clean.schema && typeof clean.schema === "object") {
|
|
10054
|
+
clean.schema = sortSchemaByPos$1(clean.schema);
|
|
10055
|
+
}
|
|
10056
|
+
const ordered = {};
|
|
10057
|
+
if (clean.name !== void 0) {
|
|
10058
|
+
ordered.name = clean.name;
|
|
10059
|
+
}
|
|
10060
|
+
if (clean.display_name !== void 0) {
|
|
10061
|
+
ordered.display_name = clean.display_name;
|
|
10062
|
+
}
|
|
10063
|
+
if (clean.is_root !== void 0) {
|
|
10064
|
+
ordered.is_root = clean.is_root;
|
|
10065
|
+
}
|
|
10066
|
+
if (clean.is_nestable !== void 0) {
|
|
10067
|
+
ordered.is_nestable = clean.is_nestable;
|
|
10068
|
+
}
|
|
10069
|
+
const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
|
|
10070
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
10071
|
+
if (!handled.has(key)) {
|
|
10072
|
+
ordered[key] = value;
|
|
10073
|
+
}
|
|
10074
|
+
}
|
|
10075
|
+
if (clean.schema !== void 0) {
|
|
10076
|
+
ordered.schema = clean.schema;
|
|
10077
|
+
}
|
|
10078
|
+
return `defineBlock(${formatValue(ordered, 0)})`;
|
|
10079
|
+
}
|
|
10080
|
+
function serializeDatasource(datasource) {
|
|
10081
|
+
const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
|
|
10082
|
+
if (Array.isArray(clean.dimensions)) {
|
|
10083
|
+
clean.dimensions = clean.dimensions.map(
|
|
10084
|
+
(dim) => typeof dim === "object" && dim !== null ? stripKeys(dim, DATASOURCE_DIMENSION_STRIP_KEYS) : dim
|
|
10085
|
+
);
|
|
10086
|
+
}
|
|
10087
|
+
const ordered = {};
|
|
10088
|
+
if (clean.name !== void 0) {
|
|
10089
|
+
ordered.name = clean.name;
|
|
10090
|
+
}
|
|
10091
|
+
if (clean.slug !== void 0) {
|
|
10092
|
+
ordered.slug = clean.slug;
|
|
10093
|
+
}
|
|
10094
|
+
const handled = /* @__PURE__ */ new Set(["name", "slug"]);
|
|
10095
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
10096
|
+
if (!handled.has(key)) {
|
|
10097
|
+
ordered[key] = value;
|
|
10098
|
+
}
|
|
10099
|
+
}
|
|
10100
|
+
return `defineDatasource(${formatValue(ordered, 0)})`;
|
|
10101
|
+
}
|
|
10102
|
+
function serializeComponentFolder(folder) {
|
|
10103
|
+
const clean = stripKeys(folder, FOLDER_STRIP_KEYS);
|
|
10104
|
+
const ordered = {};
|
|
10105
|
+
if (clean.name !== void 0) {
|
|
10106
|
+
ordered.name = clean.name;
|
|
10107
|
+
}
|
|
10108
|
+
const handled = /* @__PURE__ */ new Set(["name"]);
|
|
10109
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
10110
|
+
if (!handled.has(key)) {
|
|
10111
|
+
ordered[key] = value;
|
|
10112
|
+
}
|
|
10113
|
+
}
|
|
10114
|
+
return `defineBlockFolder(${formatValue(ordered, 0)})`;
|
|
10115
|
+
}
|
|
10116
|
+
|
|
10117
|
+
function diffEntity(type, name, localSerialized, remoteSerialized) {
|
|
10118
|
+
if (!remoteSerialized && localSerialized) {
|
|
10119
|
+
return { type, name, action: "create", diff: null, local: null, remote: null };
|
|
10120
|
+
}
|
|
10121
|
+
if (remoteSerialized && !localSerialized) {
|
|
10122
|
+
return { type, name, action: "stale", diff: null, local: null, remote: null };
|
|
10123
|
+
}
|
|
10124
|
+
if (localSerialized === remoteSerialized) {
|
|
10125
|
+
return { type, name, action: "unchanged", diff: null, local: null, remote: null };
|
|
10126
|
+
}
|
|
10127
|
+
const patch = createTwoFilesPatch(
|
|
10128
|
+
`remote/${name}`,
|
|
10129
|
+
`local/${name}`,
|
|
10130
|
+
remoteSerialized,
|
|
10131
|
+
localSerialized,
|
|
10132
|
+
"remote",
|
|
10133
|
+
"local"
|
|
10134
|
+
);
|
|
10135
|
+
return { type, name, action: "update", diff: patch, local: null, remote: null };
|
|
10136
|
+
}
|
|
10137
|
+
function diffSchema(local, remote) {
|
|
10138
|
+
const diffs = [];
|
|
10139
|
+
const processedComponentNames = /* @__PURE__ */ new Set();
|
|
10140
|
+
for (const comp of local.components) {
|
|
10141
|
+
processedComponentNames.add(comp.name);
|
|
10142
|
+
const remoteComp = remote.components.get(comp.name);
|
|
10143
|
+
const localSerialized = serializeComponent(applyDefaults(comp, COMPONENT_DEFAULTS));
|
|
10144
|
+
const remoteSerialized = remoteComp ? serializeComponent(applyDefaults(remoteComp, COMPONENT_DEFAULTS)) : null;
|
|
10145
|
+
diffs.push(diffEntity("component", comp.name, localSerialized, remoteSerialized));
|
|
10146
|
+
}
|
|
10147
|
+
for (const [name] of remote.components) {
|
|
10148
|
+
if (!processedComponentNames.has(name)) {
|
|
10149
|
+
diffs.push(diffEntity("component", name, null, "stale"));
|
|
10150
|
+
}
|
|
10151
|
+
}
|
|
10152
|
+
const processedFolderNames = /* @__PURE__ */ new Set();
|
|
10153
|
+
for (const folder of local.componentFolders) {
|
|
10154
|
+
processedFolderNames.add(folder.name);
|
|
10155
|
+
const remoteFolder = remote.componentFolders.get(folder.name);
|
|
10156
|
+
const localSerialized = serializeComponentFolder(folder);
|
|
10157
|
+
const remoteSerialized = remoteFolder ? serializeComponentFolder(remoteFolder) : null;
|
|
10158
|
+
diffs.push(diffEntity("componentFolder", folder.name, localSerialized, remoteSerialized));
|
|
10159
|
+
}
|
|
10160
|
+
for (const [name] of remote.componentFolders) {
|
|
10161
|
+
if (!processedFolderNames.has(name)) {
|
|
10162
|
+
diffs.push(diffEntity("componentFolder", name, null, "stale"));
|
|
10163
|
+
}
|
|
10164
|
+
}
|
|
10165
|
+
const processedDatasourceNames = /* @__PURE__ */ new Set();
|
|
10166
|
+
for (const ds of local.datasources) {
|
|
10167
|
+
processedDatasourceNames.add(ds.name);
|
|
10168
|
+
const remoteDs = remote.datasources.get(ds.name);
|
|
10169
|
+
const localSerialized = serializeDatasource(applyDefaults(ds, DATASOURCE_DEFAULTS));
|
|
10170
|
+
const remoteSerialized = remoteDs ? serializeDatasource(applyDefaults(remoteDs, DATASOURCE_DEFAULTS)) : null;
|
|
10171
|
+
diffs.push(diffEntity("datasource", ds.name, localSerialized, remoteSerialized));
|
|
10172
|
+
}
|
|
10173
|
+
for (const [name] of remote.datasources) {
|
|
10174
|
+
if (!processedDatasourceNames.has(name)) {
|
|
10175
|
+
diffs.push(diffEntity("datasource", name, null, "stale"));
|
|
10176
|
+
}
|
|
10177
|
+
}
|
|
10178
|
+
return {
|
|
10179
|
+
diffs,
|
|
10180
|
+
creates: diffs.filter((d) => d.action === "create").length,
|
|
10181
|
+
updates: diffs.filter((d) => d.action === "update").length,
|
|
10182
|
+
unchanged: diffs.filter((d) => d.action === "unchanged").length,
|
|
10183
|
+
stale: diffs.filter((d) => d.action === "stale").length
|
|
10184
|
+
};
|
|
10185
|
+
}
|
|
10186
|
+
|
|
10187
|
+
async function fetchRemoteSchema(spaceId) {
|
|
10188
|
+
const client = getMapiClient();
|
|
10189
|
+
const spaceIdNum = Number(spaceId);
|
|
10190
|
+
const [componentsRes, foldersRes, rawDatasources] = await Promise.all([
|
|
10191
|
+
client.components.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
|
|
10192
|
+
client.componentFolders.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
|
|
10193
|
+
fetchAllPages(
|
|
10194
|
+
(page) => client.datasources.list({ path: { space_id: spaceIdNum }, query: { page }, throwOnError: true }),
|
|
10195
|
+
(data) => data?.datasources ?? []
|
|
10196
|
+
)
|
|
10197
|
+
]);
|
|
10198
|
+
const rawComponents = componentsRes.data?.components ?? [];
|
|
10199
|
+
const rawComponentFolders = foldersRes.data?.component_groups ?? [];
|
|
10200
|
+
const remote = {
|
|
10201
|
+
components: new Map(rawComponents.map((c) => [c.name, c])),
|
|
10202
|
+
componentFolders: new Map(rawComponentFolders.map((f) => [f.name, f])),
|
|
10203
|
+
datasources: new Map(rawDatasources.map((d) => [d.name, d]))
|
|
10204
|
+
};
|
|
10205
|
+
return { remote, rawComponents, rawComponentFolders, rawDatasources };
|
|
10206
|
+
}
|
|
10207
|
+
|
|
10208
|
+
function isRecord(value) {
|
|
10209
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10210
|
+
}
|
|
10211
|
+
function isSchemaField(value) {
|
|
10212
|
+
return isRecord(value) && "type" in value;
|
|
10213
|
+
}
|
|
10214
|
+
function toSchemaRecord(schema) {
|
|
10215
|
+
const result = {};
|
|
10216
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
10217
|
+
if (key === "_uid" || key === "component" || !isSchemaField(value)) {
|
|
10218
|
+
continue;
|
|
10219
|
+
}
|
|
10220
|
+
result[key] = value;
|
|
10221
|
+
}
|
|
10222
|
+
return result;
|
|
10223
|
+
}
|
|
10224
|
+
function buildComponentPayload(input) {
|
|
10225
|
+
if (!isRecord(input)) {
|
|
10226
|
+
return { name: "" };
|
|
10227
|
+
}
|
|
10228
|
+
return {
|
|
10229
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
10230
|
+
// Fields in COMPONENT_DEFAULTS are always sent with their reset value so that
|
|
10231
|
+
// removing a field from the local schema actually clears it on the API.
|
|
10232
|
+
// (Root-level fields are additive on MAPI update — omitting preserves the old value.)
|
|
10233
|
+
display_name: typeof input.display_name === "string" ? input.display_name : "",
|
|
10234
|
+
description: typeof input.description === "string" ? input.description : "",
|
|
10235
|
+
color: typeof input.color === "string" ? input.color : "",
|
|
10236
|
+
icon: typeof input.icon === "string" ? input.icon : "",
|
|
10237
|
+
preview_field: typeof input.preview_field === "string" ? input.preview_field : "",
|
|
10238
|
+
internal_tag_ids: Array.isArray(input.internal_tag_ids) ? input.internal_tag_ids : [],
|
|
10239
|
+
// Conditionally sent: only included when explicitly set in local schema
|
|
10240
|
+
...isRecord(input.schema) && { schema: toSchemaRecord(input.schema) },
|
|
10241
|
+
...typeof input.is_root === "boolean" && { is_root: input.is_root },
|
|
10242
|
+
...typeof input.is_nestable === "boolean" && { is_nestable: input.is_nestable },
|
|
10243
|
+
...typeof input.component_group_uuid === "string" && { component_group_uuid: input.component_group_uuid }
|
|
10244
|
+
};
|
|
10245
|
+
}
|
|
10246
|
+
function toComponentCreate(input) {
|
|
10247
|
+
return buildComponentPayload(input);
|
|
10248
|
+
}
|
|
10249
|
+
function toComponentUpdate(input) {
|
|
10250
|
+
return buildComponentPayload(input);
|
|
10251
|
+
}
|
|
10252
|
+
function toComponentFolderCreate(input) {
|
|
10253
|
+
if (!isRecord(input)) {
|
|
10254
|
+
return { name: "" };
|
|
10255
|
+
}
|
|
10256
|
+
return {
|
|
10257
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
10258
|
+
...typeof input.parent_id === "number" && { parent_id: input.parent_id }
|
|
10259
|
+
};
|
|
10260
|
+
}
|
|
10261
|
+
function toDatasourceCreate(input) {
|
|
10262
|
+
if (!isRecord(input)) {
|
|
10263
|
+
return { name: "", slug: "" };
|
|
10264
|
+
}
|
|
10265
|
+
const result = {
|
|
10266
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
10267
|
+
slug: typeof input.slug === "string" ? input.slug : ""
|
|
10268
|
+
};
|
|
10269
|
+
if (Array.isArray(input.dimensions)) {
|
|
10270
|
+
result.dimensions_attributes = input.dimensions.filter((d) => isRecord(d) && typeof d.name === "string" && typeof d.entry_value === "string").map((d) => ({
|
|
10271
|
+
name: d.name,
|
|
10272
|
+
entry_value: d.entry_value
|
|
10273
|
+
}));
|
|
10274
|
+
}
|
|
10275
|
+
return result;
|
|
10276
|
+
}
|
|
10277
|
+
function toDatasourceUpdate(input, remote) {
|
|
10278
|
+
const base = toDatasourceCreate(input);
|
|
10279
|
+
const localDims = base.dimensions_attributes ?? [];
|
|
10280
|
+
const remoteDims = remote.dimensions ?? [];
|
|
10281
|
+
if (remoteDims.length === 0) {
|
|
10282
|
+
return base;
|
|
10283
|
+
}
|
|
10284
|
+
const localKeys = new Set(localDims.map((d) => `${d.name}::${d.entry_value}`));
|
|
10285
|
+
const destroyEntries = remoteDims.filter((rd) => rd.id != null && !localKeys.has(`${rd.name}::${rd.entry_value}`)).map((rd) => ({ id: rd.id, _destroy: true }));
|
|
10286
|
+
if (destroyEntries.length > 0) {
|
|
10287
|
+
return {
|
|
10288
|
+
...base,
|
|
10289
|
+
dimensions_attributes: [...localDims, ...destroyEntries]
|
|
10290
|
+
};
|
|
10291
|
+
}
|
|
10292
|
+
return base;
|
|
10293
|
+
}
|
|
10294
|
+
|
|
10295
|
+
function formatDiffOutput(result, options) {
|
|
10296
|
+
const lines = [];
|
|
10297
|
+
const byType = {
|
|
10298
|
+
component: [],
|
|
10299
|
+
componentFolder: [],
|
|
10300
|
+
datasource: []
|
|
10301
|
+
};
|
|
10302
|
+
for (const diff of result.diffs) {
|
|
10303
|
+
byType[diff.type].push(diff);
|
|
10304
|
+
}
|
|
10305
|
+
const willDelete = options?.delete ?? false;
|
|
10306
|
+
const icons = {
|
|
10307
|
+
create: chalk.green("+"),
|
|
10308
|
+
update: chalk.yellow("~"),
|
|
10309
|
+
unchanged: chalk.dim("="),
|
|
10310
|
+
stale: chalk.red("-")
|
|
10311
|
+
};
|
|
10312
|
+
const sections = [
|
|
10313
|
+
["Components", byType.component],
|
|
10314
|
+
["Component Folders", byType.componentFolder],
|
|
10315
|
+
["Datasources", byType.datasource]
|
|
10316
|
+
];
|
|
10317
|
+
for (const [label, diffs] of sections) {
|
|
10318
|
+
if (diffs.length === 0) {
|
|
10319
|
+
continue;
|
|
10320
|
+
}
|
|
10321
|
+
lines.push(chalk.bold(label));
|
|
10322
|
+
for (const diff of diffs) {
|
|
10323
|
+
const icon = icons[diff.action] ?? " ";
|
|
10324
|
+
const name = diff.action === "stale" ? chalk.red(diff.name) : diff.name;
|
|
10325
|
+
const actionLabel = diff.action === "stale" && willDelete ? "delete" : diff.action;
|
|
10326
|
+
lines.push(` ${icon} ${name} ${chalk.dim(`(${actionLabel})`)}`);
|
|
10327
|
+
if (diff.diff) {
|
|
10328
|
+
for (const line of diff.diff.split("\n")) {
|
|
10329
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
10330
|
+
lines.push(` ${chalk.green(line)}`);
|
|
10331
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
10332
|
+
lines.push(` ${chalk.red(line)}`);
|
|
10333
|
+
}
|
|
10334
|
+
}
|
|
10335
|
+
}
|
|
10336
|
+
}
|
|
10337
|
+
lines.push("");
|
|
10338
|
+
}
|
|
10339
|
+
const summary = [
|
|
10340
|
+
result.creates > 0 ? chalk.green(`${result.creates} to create`) : null,
|
|
10341
|
+
result.updates > 0 ? chalk.yellow(`${result.updates} to update`) : null,
|
|
10342
|
+
result.unchanged > 0 ? chalk.dim(`${result.unchanged} unchanged`) : null,
|
|
10343
|
+
result.stale > 0 ? chalk.red(`${result.stale} ${willDelete ? "to delete" : "stale"}`) : null
|
|
10344
|
+
].filter(Boolean).join(", ");
|
|
10345
|
+
lines.push(`Summary: ${summary}`);
|
|
10346
|
+
return lines.join("\n");
|
|
10347
|
+
}
|
|
10348
|
+
async function executePush(spaceId, local, remote, diffResult, options) {
|
|
10349
|
+
const client = getMapiClient();
|
|
10350
|
+
const spaceIdNum = Number(spaceId);
|
|
10351
|
+
let created = 0;
|
|
10352
|
+
let updated = 0;
|
|
10353
|
+
let deleted = 0;
|
|
10354
|
+
const createdFolderUuids = /* @__PURE__ */ new Map();
|
|
10355
|
+
const folderDiffs = diffResult.diffs.filter((d) => d.type === "componentFolder");
|
|
10356
|
+
const folderResults = await Promise.allSettled(
|
|
10357
|
+
folderDiffs.map(async (diff) => {
|
|
10358
|
+
const localFolder = local.componentFolders.find((f) => f.name === diff.name);
|
|
10359
|
+
if (diff.action === "create" && localFolder) {
|
|
10360
|
+
const response = await client.componentFolders.create({
|
|
10361
|
+
path: { space_id: spaceIdNum },
|
|
10362
|
+
body: { component_group: toComponentFolderCreate(localFolder) },
|
|
10363
|
+
throwOnError: true
|
|
10364
|
+
});
|
|
10365
|
+
return { action: "created", uuid: response.data?.component_group?.uuid };
|
|
10366
|
+
}
|
|
10367
|
+
if (diff.action === "update" && localFolder) {
|
|
10368
|
+
const existing = remote.componentFolders.get(diff.name);
|
|
10369
|
+
if (existing?.id) {
|
|
10370
|
+
await client.componentFolders.update(existing.id, {
|
|
10371
|
+
path: { space_id: spaceIdNum },
|
|
10372
|
+
body: { component_group: toComponentFolderCreate(localFolder) },
|
|
10373
|
+
throwOnError: true
|
|
10374
|
+
});
|
|
10375
|
+
return { action: "updated" };
|
|
10376
|
+
}
|
|
10377
|
+
}
|
|
10378
|
+
})
|
|
10379
|
+
);
|
|
10380
|
+
for (let i = 0; i < folderResults.length; i++) {
|
|
10381
|
+
const result = folderResults[i];
|
|
10382
|
+
const diff = folderDiffs[i];
|
|
10383
|
+
if (result.status === "fulfilled") {
|
|
10384
|
+
if (result.value?.action === "created") {
|
|
10385
|
+
if (result.value.uuid) {
|
|
10386
|
+
createdFolderUuids.set(diff.name, result.value.uuid);
|
|
10387
|
+
}
|
|
10388
|
+
created++;
|
|
10389
|
+
} else if (result.value?.action === "updated") {
|
|
10390
|
+
updated++;
|
|
10391
|
+
}
|
|
10392
|
+
} else {
|
|
10393
|
+
const eventId = diff.action === "create" ? "push_component_group" : "update_component_group";
|
|
10394
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} component folder ${diff.name}`);
|
|
10395
|
+
}
|
|
10396
|
+
}
|
|
10397
|
+
const skippedComponents = /* @__PURE__ */ new Set();
|
|
10398
|
+
if (options.pendingFolderAssignments) {
|
|
10399
|
+
const logger = getLogger();
|
|
10400
|
+
for (const [folderName, componentNames] of options.pendingFolderAssignments) {
|
|
10401
|
+
const remoteUuid = createdFolderUuids.get(folderName);
|
|
10402
|
+
if (!remoteUuid) {
|
|
10403
|
+
logger.warn(`Could not resolve folder '${folderName}' \u2014 skipping components: ${componentNames.join(", ")}`);
|
|
10404
|
+
for (const name of componentNames) {
|
|
10405
|
+
skippedComponents.add(name);
|
|
10406
|
+
}
|
|
10407
|
+
continue;
|
|
10408
|
+
}
|
|
10409
|
+
for (const compName of componentNames) {
|
|
10410
|
+
const comp = local.components.find((c) => c.name === compName);
|
|
10411
|
+
if (comp) {
|
|
10412
|
+
comp.component_group_uuid = remoteUuid;
|
|
10413
|
+
}
|
|
10414
|
+
}
|
|
10415
|
+
}
|
|
10416
|
+
}
|
|
10417
|
+
const componentDiffs = diffResult.diffs.filter((d) => d.type === "component" && !skippedComponents.has(d.name));
|
|
10418
|
+
const componentResults = await Promise.allSettled(
|
|
10419
|
+
componentDiffs.map(async (diff) => {
|
|
10420
|
+
const localComp = local.components.find((c) => c.name === diff.name);
|
|
10421
|
+
if (diff.action === "create" && localComp) {
|
|
10422
|
+
await client.components.create({
|
|
10423
|
+
path: { space_id: spaceIdNum },
|
|
10424
|
+
body: { component: toComponentCreate(localComp) },
|
|
10425
|
+
throwOnError: true
|
|
10426
|
+
});
|
|
10427
|
+
return "created";
|
|
10428
|
+
}
|
|
10429
|
+
if (diff.action === "update" && localComp) {
|
|
10430
|
+
const existing = remote.components.get(diff.name);
|
|
10431
|
+
if (existing?.id) {
|
|
10432
|
+
await client.components.update(existing.id, {
|
|
10433
|
+
path: { space_id: spaceIdNum },
|
|
10434
|
+
body: { component: toComponentUpdate(localComp) },
|
|
10435
|
+
throwOnError: true
|
|
10436
|
+
});
|
|
10437
|
+
return "updated";
|
|
10438
|
+
}
|
|
10439
|
+
}
|
|
10440
|
+
})
|
|
10441
|
+
);
|
|
10442
|
+
for (let i = 0; i < componentResults.length; i++) {
|
|
10443
|
+
const result = componentResults[i];
|
|
10444
|
+
const diff = componentDiffs[i];
|
|
10445
|
+
if (result.status === "fulfilled") {
|
|
10446
|
+
if (result.value === "created") {
|
|
10447
|
+
created++;
|
|
10448
|
+
} else if (result.value === "updated") {
|
|
10449
|
+
updated++;
|
|
10450
|
+
}
|
|
10451
|
+
} else {
|
|
10452
|
+
const eventId = diff.action === "create" ? "push_component" : "update_component";
|
|
10453
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} component ${diff.name}`);
|
|
10454
|
+
}
|
|
10455
|
+
}
|
|
10456
|
+
const datasourceDiffs = diffResult.diffs.filter((d) => d.type === "datasource");
|
|
10457
|
+
const datasourceResults = await Promise.allSettled(
|
|
10458
|
+
datasourceDiffs.map(async (diff) => {
|
|
10459
|
+
const localDs = local.datasources.find((d) => d.name === diff.name);
|
|
10460
|
+
if (diff.action === "create" && localDs) {
|
|
10461
|
+
await client.datasources.create({
|
|
10462
|
+
path: { space_id: spaceIdNum },
|
|
10463
|
+
body: { datasource: toDatasourceCreate(localDs) },
|
|
10464
|
+
throwOnError: true
|
|
10465
|
+
});
|
|
10466
|
+
return "created";
|
|
10467
|
+
}
|
|
10468
|
+
if (diff.action === "update" && localDs) {
|
|
10469
|
+
const existing = remote.datasources.get(diff.name);
|
|
10470
|
+
if (existing?.id) {
|
|
10471
|
+
await client.datasources.update(existing.id, {
|
|
10472
|
+
path: { space_id: spaceIdNum },
|
|
10473
|
+
body: { datasource: toDatasourceUpdate(localDs, existing) },
|
|
10474
|
+
throwOnError: true
|
|
10475
|
+
});
|
|
10476
|
+
return "updated";
|
|
10477
|
+
}
|
|
10478
|
+
}
|
|
10479
|
+
})
|
|
10480
|
+
);
|
|
10481
|
+
for (let i = 0; i < datasourceResults.length; i++) {
|
|
10482
|
+
const result = datasourceResults[i];
|
|
10483
|
+
const diff = datasourceDiffs[i];
|
|
10484
|
+
if (result.status === "fulfilled") {
|
|
10485
|
+
if (result.value === "created") {
|
|
10486
|
+
created++;
|
|
10487
|
+
} else if (result.value === "updated") {
|
|
10488
|
+
updated++;
|
|
10489
|
+
}
|
|
10490
|
+
} else {
|
|
10491
|
+
const eventId = diff.action === "create" ? "push_datasource" : "update_datasource";
|
|
10492
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} datasource ${diff.name}`);
|
|
10493
|
+
}
|
|
10494
|
+
}
|
|
10495
|
+
if (options.delete) {
|
|
10496
|
+
const staleComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
|
|
10497
|
+
const deleteComponentResults = await Promise.allSettled(
|
|
10498
|
+
staleComponents.map(async (diff) => {
|
|
10499
|
+
const existing = remote.components.get(diff.name);
|
|
10500
|
+
if (existing?.id) {
|
|
10501
|
+
await client.components.delete(existing.id, {
|
|
10502
|
+
path: { space_id: spaceIdNum },
|
|
10503
|
+
throwOnError: true
|
|
10504
|
+
});
|
|
10505
|
+
return true;
|
|
10506
|
+
}
|
|
10507
|
+
})
|
|
10508
|
+
);
|
|
10509
|
+
for (let i = 0; i < deleteComponentResults.length; i++) {
|
|
10510
|
+
const result = deleteComponentResults[i];
|
|
10511
|
+
if (result.status === "fulfilled") {
|
|
10512
|
+
if (result.value) {
|
|
10513
|
+
deleted++;
|
|
10514
|
+
}
|
|
10515
|
+
} else {
|
|
10516
|
+
handleAPIError("push_component", result.reason, `Failed to delete component ${staleComponents[i].name}`);
|
|
10517
|
+
}
|
|
10518
|
+
}
|
|
10519
|
+
const staleDatasources = diffResult.diffs.filter((d) => d.type === "datasource" && d.action === "stale");
|
|
10520
|
+
const deleteDatasourceResults = await Promise.allSettled(
|
|
10521
|
+
staleDatasources.map(async (diff) => {
|
|
10522
|
+
const existing = remote.datasources.get(diff.name);
|
|
10523
|
+
if (existing?.id) {
|
|
10524
|
+
await client.datasources.delete(existing.id, {
|
|
10525
|
+
path: { space_id: spaceIdNum },
|
|
10526
|
+
throwOnError: true
|
|
10527
|
+
});
|
|
10528
|
+
return true;
|
|
10529
|
+
}
|
|
10530
|
+
})
|
|
10531
|
+
);
|
|
10532
|
+
for (let i = 0; i < deleteDatasourceResults.length; i++) {
|
|
10533
|
+
const result = deleteDatasourceResults[i];
|
|
10534
|
+
if (result.status === "fulfilled") {
|
|
10535
|
+
if (result.value) {
|
|
10536
|
+
deleted++;
|
|
10537
|
+
}
|
|
10538
|
+
} else {
|
|
10539
|
+
handleAPIError("delete_datasource", result.reason, `Failed to delete datasource ${staleDatasources[i].name}`);
|
|
10540
|
+
}
|
|
10541
|
+
}
|
|
10542
|
+
const staleFolders = diffResult.diffs.filter((d) => d.type === "componentFolder" && d.action === "stale");
|
|
10543
|
+
const deleteFolderResults = await Promise.allSettled(
|
|
10544
|
+
staleFolders.map(async (diff) => {
|
|
10545
|
+
const existing = remote.componentFolders.get(diff.name);
|
|
10546
|
+
if (existing?.id) {
|
|
10547
|
+
await client.componentFolders.delete(existing.id, {
|
|
10548
|
+
path: { space_id: spaceIdNum },
|
|
10549
|
+
throwOnError: true
|
|
10550
|
+
});
|
|
10551
|
+
return true;
|
|
10552
|
+
}
|
|
10553
|
+
})
|
|
10554
|
+
);
|
|
10555
|
+
for (let i = 0; i < deleteFolderResults.length; i++) {
|
|
10556
|
+
const result = deleteFolderResults[i];
|
|
10557
|
+
if (result.status === "fulfilled") {
|
|
10558
|
+
if (result.value) {
|
|
10559
|
+
deleted++;
|
|
10560
|
+
}
|
|
10561
|
+
} else {
|
|
10562
|
+
handleAPIError("push_component_group", result.reason, `Failed to delete folder ${staleFolders[i].name}`);
|
|
10563
|
+
}
|
|
10564
|
+
}
|
|
10565
|
+
}
|
|
10566
|
+
return { created, updated, deleted };
|
|
10567
|
+
}
|
|
10568
|
+
function buildChangesetEntries(diffResult, local, remote, options) {
|
|
10569
|
+
const changes = [];
|
|
10570
|
+
for (const diff of diffResult.diffs) {
|
|
10571
|
+
if (diff.action === "unchanged") {
|
|
10572
|
+
continue;
|
|
10573
|
+
}
|
|
10574
|
+
if (diff.action === "stale" && !options.delete) {
|
|
10575
|
+
continue;
|
|
10576
|
+
}
|
|
10577
|
+
const action = diff.action === "stale" ? "delete" : diff.action;
|
|
10578
|
+
let remoteSrc;
|
|
10579
|
+
let localSrc;
|
|
10580
|
+
if (diff.type === "component") {
|
|
10581
|
+
remoteSrc = remote.components.get(diff.name);
|
|
10582
|
+
localSrc = local.components.find((c) => c.name === diff.name);
|
|
10583
|
+
} else if (diff.type === "componentFolder") {
|
|
10584
|
+
remoteSrc = remote.componentFolders.get(diff.name);
|
|
10585
|
+
localSrc = local.componentFolders.find((f) => f.name === diff.name);
|
|
10586
|
+
} else if (diff.type === "datasource") {
|
|
10587
|
+
remoteSrc = remote.datasources.get(diff.name);
|
|
10588
|
+
localSrc = local.datasources.find((d) => d.name === diff.name);
|
|
10589
|
+
}
|
|
10590
|
+
changes.push({
|
|
10591
|
+
type: diff.type,
|
|
10592
|
+
name: diff.name,
|
|
10593
|
+
action,
|
|
10594
|
+
...remoteSrc && { before: { ...remoteSrc } },
|
|
10595
|
+
...localSrc && { after: { ...localSrc } }
|
|
10596
|
+
});
|
|
10597
|
+
}
|
|
10598
|
+
return changes;
|
|
10599
|
+
}
|
|
10600
|
+
|
|
10601
|
+
function findRemoteFolderByUuid(remoteFolders, uuid) {
|
|
10602
|
+
for (const folder of remoteFolders.values()) {
|
|
10603
|
+
if (folder.uuid === uuid) {
|
|
10604
|
+
return folder;
|
|
10605
|
+
}
|
|
10606
|
+
}
|
|
10607
|
+
return void 0;
|
|
10608
|
+
}
|
|
10609
|
+
function resolveFolderReferences(local, remote) {
|
|
10610
|
+
const localUuidToName = /* @__PURE__ */ new Map();
|
|
10611
|
+
for (const folder of local.componentFolders) {
|
|
10612
|
+
if (folder.uuid) {
|
|
10613
|
+
localUuidToName.set(folder.uuid, folder.name);
|
|
10614
|
+
}
|
|
10615
|
+
}
|
|
10616
|
+
const pendingFolderAssignments = /* @__PURE__ */ new Map();
|
|
10617
|
+
const resolvedComponents = local.components.map((comp) => {
|
|
10618
|
+
if (!comp.component_group_uuid) {
|
|
10619
|
+
return comp;
|
|
10620
|
+
}
|
|
10621
|
+
const folderName = localUuidToName.get(comp.component_group_uuid);
|
|
10622
|
+
if (!folderName) {
|
|
10623
|
+
return comp;
|
|
10624
|
+
}
|
|
10625
|
+
const remoteByUuid = findRemoteFolderByUuid(remote.componentFolders, comp.component_group_uuid);
|
|
10626
|
+
if (remoteByUuid) {
|
|
10627
|
+
return comp;
|
|
10628
|
+
}
|
|
10629
|
+
const remoteByName = remote.componentFolders.get(folderName);
|
|
10630
|
+
if (remoteByName) {
|
|
10631
|
+
return { ...comp, component_group_uuid: remoteByName.uuid };
|
|
10632
|
+
}
|
|
10633
|
+
const pending = pendingFolderAssignments.get(folderName) ?? [];
|
|
10634
|
+
pending.push(comp.name);
|
|
10635
|
+
pendingFolderAssignments.set(folderName, pending);
|
|
10636
|
+
return comp;
|
|
10637
|
+
});
|
|
10638
|
+
return {
|
|
10639
|
+
resolved: { ...local, components: resolvedComponents },
|
|
10640
|
+
pendingFolderAssignments
|
|
10641
|
+
};
|
|
10642
|
+
}
|
|
10643
|
+
|
|
10644
|
+
async function ensureDir(dir) {
|
|
10645
|
+
await mkdir(dir, { recursive: true });
|
|
10646
|
+
}
|
|
10647
|
+
async function saveChangeset(basePath, data) {
|
|
10648
|
+
const dir = join(basePath, "schema", "changesets");
|
|
10649
|
+
await ensureDir(dir);
|
|
10650
|
+
const fileName = `${fileTimestamp(data.timestamp)}.json`;
|
|
10651
|
+
const filePath = join(dir, fileName);
|
|
10652
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
10653
|
+
return filePath;
|
|
10654
|
+
}
|
|
10655
|
+
|
|
10656
|
+
const SENTINEL_FIELDS = /* @__PURE__ */ new Set(["_uid", "component"]);
|
|
10657
|
+
function classifyFieldChanges(remoteSchema, localSchema) {
|
|
10658
|
+
const removed = [];
|
|
10659
|
+
const added = [];
|
|
10660
|
+
const typeChanged = [];
|
|
10661
|
+
const requiredAdded = [];
|
|
10662
|
+
const requiredChanged = [];
|
|
10663
|
+
for (const [field, remoteField] of Object.entries(remoteSchema)) {
|
|
10664
|
+
if (SENTINEL_FIELDS.has(field)) {
|
|
10665
|
+
continue;
|
|
10666
|
+
}
|
|
10667
|
+
if (typeof remoteField.type !== "string") {
|
|
10668
|
+
continue;
|
|
10669
|
+
}
|
|
10670
|
+
if (!(field in localSchema)) {
|
|
10671
|
+
removed.push({ field, type: remoteField.type });
|
|
10672
|
+
}
|
|
10673
|
+
}
|
|
10674
|
+
for (const [field, localField] of Object.entries(localSchema)) {
|
|
10675
|
+
if (SENTINEL_FIELDS.has(field)) {
|
|
10676
|
+
continue;
|
|
10677
|
+
}
|
|
10678
|
+
if (typeof localField.type !== "string") {
|
|
10679
|
+
continue;
|
|
10680
|
+
}
|
|
10681
|
+
if (!(field in remoteSchema)) {
|
|
10682
|
+
if (localField.required) {
|
|
10683
|
+
requiredAdded.push({ field, type: localField.type });
|
|
10684
|
+
} else {
|
|
10685
|
+
added.push({ field, type: localField.type, required: false });
|
|
10686
|
+
}
|
|
10687
|
+
} else {
|
|
10688
|
+
const remoteField = remoteSchema[field];
|
|
10689
|
+
if (typeof remoteField?.type !== "string") {
|
|
10690
|
+
continue;
|
|
10691
|
+
}
|
|
10692
|
+
if (remoteField.type !== localField.type) {
|
|
10693
|
+
typeChanged.push({ field, oldType: remoteField.type, newType: localField.type });
|
|
10694
|
+
}
|
|
10695
|
+
if (localField.required && !remoteField.required) {
|
|
10696
|
+
requiredChanged.push({ field, type: localField.type });
|
|
10697
|
+
}
|
|
10698
|
+
}
|
|
10699
|
+
}
|
|
10700
|
+
return { removed, added, typeChanged, requiredAdded, requiredChanged };
|
|
10701
|
+
}
|
|
10702
|
+
function longestCommonSubstring(a, b) {
|
|
10703
|
+
let maxLen = 0;
|
|
10704
|
+
for (let i = 0; i < a.length; i++) {
|
|
10705
|
+
for (let j = 0; j < b.length; j++) {
|
|
10706
|
+
let len = 0;
|
|
10707
|
+
while (i + len < a.length && j + len < b.length && a[i + len] === b[j + len]) {
|
|
10708
|
+
len++;
|
|
10709
|
+
}
|
|
10710
|
+
if (len > maxLen) {
|
|
10711
|
+
maxLen = len;
|
|
10712
|
+
}
|
|
10713
|
+
}
|
|
10714
|
+
}
|
|
10715
|
+
return maxLen;
|
|
10716
|
+
}
|
|
10717
|
+
function nameSimilarity(a, b) {
|
|
10718
|
+
const longer = Math.max(a.length, b.length);
|
|
10719
|
+
if (longer === 0) {
|
|
10720
|
+
return 1;
|
|
10721
|
+
}
|
|
10722
|
+
return longestCommonSubstring(a, b) / longer;
|
|
10723
|
+
}
|
|
10724
|
+
function detectRenames(removed, added) {
|
|
10725
|
+
const renames = [];
|
|
10726
|
+
const usedRemoved = /* @__PURE__ */ new Set();
|
|
10727
|
+
const usedAdded = /* @__PURE__ */ new Set();
|
|
10728
|
+
const addedByType = /* @__PURE__ */ new Map();
|
|
10729
|
+
for (const addedField of added) {
|
|
10730
|
+
if (!addedByType.has(addedField.type)) {
|
|
10731
|
+
addedByType.set(addedField.type, []);
|
|
10732
|
+
}
|
|
10733
|
+
addedByType.get(addedField.type).push(addedField);
|
|
10734
|
+
}
|
|
10735
|
+
const isSinglePair = removed.length === 1 && added.length === 1;
|
|
10736
|
+
for (const removedField of removed) {
|
|
10737
|
+
const candidates = addedByType.get(removedField.type) ?? [];
|
|
10738
|
+
const availableCandidates = candidates.filter((c) => !usedAdded.has(c.field));
|
|
10739
|
+
if (availableCandidates.length === 0) {
|
|
10740
|
+
continue;
|
|
10741
|
+
}
|
|
10742
|
+
let bestCandidate = availableCandidates[0];
|
|
10743
|
+
let bestScore = nameSimilarity(removedField.field, bestCandidate.field);
|
|
10744
|
+
for (let i = 1; i < availableCandidates.length; i++) {
|
|
10745
|
+
const score = nameSimilarity(removedField.field, availableCandidates[i].field);
|
|
10746
|
+
if (score > bestScore) {
|
|
10747
|
+
bestScore = score;
|
|
10748
|
+
bestCandidate = availableCandidates[i];
|
|
10749
|
+
}
|
|
10750
|
+
}
|
|
10751
|
+
if (!isSinglePair && bestScore < 0.3) {
|
|
10752
|
+
continue;
|
|
10753
|
+
}
|
|
10754
|
+
renames.push({ oldField: removedField.field, newField: bestCandidate.field, fieldType: removedField.type });
|
|
10755
|
+
usedRemoved.add(removedField.field);
|
|
10756
|
+
usedAdded.add(bestCandidate.field);
|
|
10757
|
+
}
|
|
10758
|
+
const unmatchedRemoved = removed.filter((r) => !usedRemoved.has(r.field));
|
|
10759
|
+
const unmatchedAdded = added.filter((a) => !usedAdded.has(a.field));
|
|
10760
|
+
return { renames, unmatchedRemoved, unmatchedAdded };
|
|
10761
|
+
}
|
|
10762
|
+
function analyzeBreakingChanges(diffResult, local, remote) {
|
|
10763
|
+
const results = [];
|
|
10764
|
+
const updatedComponents = diffResult.diffs.filter(
|
|
10765
|
+
(d) => d.type === "component" && d.action === "update"
|
|
10766
|
+
);
|
|
10767
|
+
for (const diff of updatedComponents) {
|
|
10768
|
+
const localComp = local.components.find((c) => c.name === diff.name);
|
|
10769
|
+
const remoteComp = remote.components.get(diff.name);
|
|
10770
|
+
if (!localComp?.schema || !remoteComp?.schema) {
|
|
10771
|
+
continue;
|
|
10772
|
+
}
|
|
10773
|
+
const classification = classifyFieldChanges(
|
|
10774
|
+
remoteComp.schema,
|
|
10775
|
+
localComp.schema
|
|
10776
|
+
);
|
|
10777
|
+
const changes = [];
|
|
10778
|
+
const { renames, unmatchedRemoved } = detectRenames(classification.removed, classification.added);
|
|
10779
|
+
for (const rename of renames) {
|
|
10780
|
+
changes.push({ kind: "rename", field: rename.newField, oldField: rename.oldField });
|
|
10781
|
+
}
|
|
10782
|
+
for (const removed of unmatchedRemoved) {
|
|
10783
|
+
changes.push({ kind: "removed", field: removed.field });
|
|
10784
|
+
}
|
|
10785
|
+
for (const tc of classification.typeChanged) {
|
|
10786
|
+
changes.push({ kind: "type_changed", field: tc.field, oldType: tc.oldType, newType: tc.newType });
|
|
10787
|
+
}
|
|
10788
|
+
for (const ra of classification.requiredAdded) {
|
|
10789
|
+
changes.push({ kind: "required_added", field: ra.field, fieldType: ra.type });
|
|
10790
|
+
}
|
|
10791
|
+
for (const rc of classification.requiredChanged) {
|
|
10792
|
+
changes.push({ kind: "required_changed", field: rc.field, fieldType: rc.type });
|
|
10793
|
+
}
|
|
10794
|
+
if (changes.length > 0) {
|
|
10795
|
+
results.push({ componentName: diff.name, changes });
|
|
10796
|
+
}
|
|
10797
|
+
}
|
|
10798
|
+
return results;
|
|
10799
|
+
}
|
|
10800
|
+
|
|
10801
|
+
const COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["text:textarea", "textarea:text"]);
|
|
10802
|
+
function defaultForType(fieldType) {
|
|
10803
|
+
switch (fieldType) {
|
|
10804
|
+
case "text":
|
|
10805
|
+
case "textarea":
|
|
10806
|
+
case "markdown":
|
|
10807
|
+
return `''`;
|
|
10808
|
+
case "number":
|
|
10809
|
+
return "0";
|
|
10810
|
+
case "boolean":
|
|
10811
|
+
return "false";
|
|
10812
|
+
default:
|
|
10813
|
+
return null;
|
|
10814
|
+
}
|
|
10815
|
+
}
|
|
10816
|
+
function typeConversion(field, oldType, newType) {
|
|
10817
|
+
const key = `${oldType}:${newType}`;
|
|
10818
|
+
if (COMPATIBLE_TYPES.has(key)) {
|
|
10819
|
+
return null;
|
|
10820
|
+
}
|
|
10821
|
+
const accessor = `block.${field}`;
|
|
10822
|
+
switch (key) {
|
|
10823
|
+
case "text:number":
|
|
10824
|
+
return `${accessor} = Number(${accessor}) || 0;`;
|
|
10825
|
+
case "number:text":
|
|
10826
|
+
return `${accessor} = String(${accessor});`;
|
|
10827
|
+
case "text:boolean":
|
|
10828
|
+
return `${accessor} = !!${accessor};`;
|
|
10829
|
+
case "boolean:text":
|
|
10830
|
+
return `${accessor} = String(${accessor});`;
|
|
10831
|
+
default:
|
|
10832
|
+
return `${accessor}; // TODO: convert from ${oldType} to ${newType}`;
|
|
10833
|
+
}
|
|
10834
|
+
}
|
|
10835
|
+
function renderMigrationCode(changes) {
|
|
10836
|
+
const lines = [];
|
|
10837
|
+
lines.push(" // Review this migration before running it against your space.");
|
|
10838
|
+
lines.push(" // Generated migrations are scaffolds and may need manual adjustments.");
|
|
10839
|
+
lines.push(" // Example rename migration:");
|
|
10840
|
+
lines.push(" // block.new_field = block.old_field;");
|
|
10841
|
+
lines.push(" // delete block.old_field;");
|
|
10842
|
+
lines.push("");
|
|
10843
|
+
for (const change of changes) {
|
|
10844
|
+
switch (change.kind) {
|
|
10845
|
+
case "rename":
|
|
10846
|
+
lines.push(` // Rename: ${change.oldField} \u2192 ${change.field}`);
|
|
10847
|
+
lines.push(` if ('${change.oldField}' in block) {`);
|
|
10848
|
+
lines.push(` block.${change.field} = block.${change.oldField};`);
|
|
10849
|
+
lines.push(` delete block.${change.oldField};`);
|
|
10850
|
+
lines.push(` }`);
|
|
10851
|
+
break;
|
|
10852
|
+
case "removed":
|
|
10853
|
+
if (change.renameHint) {
|
|
10854
|
+
lines.push(` // If '${change.field}' was renamed to '${change.renameHint.newField}', uncomment:`);
|
|
10855
|
+
lines.push(` // block.${change.renameHint.newField} = block.${change.field};`);
|
|
10856
|
+
} else {
|
|
10857
|
+
lines.push(` // Removed field: ${change.field}`);
|
|
10858
|
+
}
|
|
10859
|
+
lines.push(` delete block.${change.field};`);
|
|
10860
|
+
break;
|
|
10861
|
+
case "type_changed": {
|
|
10862
|
+
const conversion = typeConversion(change.field, change.oldType, change.newType);
|
|
10863
|
+
if (conversion) {
|
|
10864
|
+
lines.push(` // Type change: ${change.field} (${change.oldType} \u2192 ${change.newType})`);
|
|
10865
|
+
lines.push(` ${conversion}`);
|
|
10866
|
+
}
|
|
10867
|
+
break;
|
|
10868
|
+
}
|
|
10869
|
+
case "required_added": {
|
|
10870
|
+
const defaultValue = defaultForType(change.fieldType);
|
|
10871
|
+
lines.push(` // New required field: ${change.field} (${change.fieldType})`);
|
|
10872
|
+
if (defaultValue !== null) {
|
|
10873
|
+
lines.push(` // TODO: provide a meaningful default value`);
|
|
10874
|
+
lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
|
|
10875
|
+
} else {
|
|
10876
|
+
lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
|
|
10877
|
+
lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
|
|
10878
|
+
}
|
|
10879
|
+
break;
|
|
10880
|
+
}
|
|
10881
|
+
case "required_changed": {
|
|
10882
|
+
const defaultValue = defaultForType(change.fieldType);
|
|
10883
|
+
lines.push(` // Field is now required: ${change.field} (${change.fieldType})`);
|
|
10884
|
+
lines.push(` // Existing stories may have null/undefined values \u2014 provide a default for those.`);
|
|
10885
|
+
if (defaultValue !== null) {
|
|
10886
|
+
lines.push(` // TODO: provide a meaningful default value`);
|
|
10887
|
+
lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
|
|
10888
|
+
} else {
|
|
10889
|
+
lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
|
|
10890
|
+
lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
|
|
10891
|
+
}
|
|
10892
|
+
break;
|
|
10893
|
+
}
|
|
10894
|
+
}
|
|
10895
|
+
lines.push("");
|
|
10896
|
+
}
|
|
10897
|
+
const body = lines.length > 0 ? `
|
|
10898
|
+
${lines.join("\n")}` : "\n";
|
|
10899
|
+
return `export default function (block) {${body} return block;
|
|
10900
|
+
}
|
|
10901
|
+
`;
|
|
10902
|
+
}
|
|
10903
|
+
async function writeMigrationFile(options) {
|
|
10904
|
+
const { spaceId, componentName, code, timestamp, basePath } = options;
|
|
10905
|
+
const dir = resolvePath(basePath, `migrations/${spaceId}`);
|
|
10906
|
+
await mkdir(dir, { recursive: true });
|
|
10907
|
+
const fileName = `${componentName}.${fileTimestamp(timestamp)}.js`;
|
|
10908
|
+
const filePath = join(dir, fileName);
|
|
10909
|
+
await writeFile(filePath, code, "utf-8");
|
|
10910
|
+
return filePath;
|
|
10911
|
+
}
|
|
10912
|
+
|
|
10913
|
+
const DEFAULT_GROUPS_FILENAME = "groups.json";
|
|
10914
|
+
const CONSOLIDATED_COMPONENTS_FILENAME = "components.json";
|
|
10915
|
+
async function writeLocalComponents({
|
|
10916
|
+
space,
|
|
10917
|
+
basePath,
|
|
10918
|
+
resolved,
|
|
10919
|
+
diffResult,
|
|
10920
|
+
deleteRemoved,
|
|
10921
|
+
ui,
|
|
10922
|
+
logger
|
|
10923
|
+
}) {
|
|
10924
|
+
const componentsDir = resolveCommandPath(directories.components, space, basePath);
|
|
10925
|
+
const consolidatedPath = join(componentsDir, CONSOLIDATED_COMPONENTS_FILENAME);
|
|
10926
|
+
if (await fileExists(consolidatedPath)) {
|
|
10927
|
+
ui.warn(
|
|
10928
|
+
`A consolidated ${CONSOLIDATED_COMPONENTS_FILENAME} exists at ${displayPath(componentsDir, basePath)}. 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.`
|
|
10929
|
+
);
|
|
10930
|
+
}
|
|
10931
|
+
for (const component of resolved.components) {
|
|
10932
|
+
const filePath = join(componentsDir, `${sanitizeFilename(component.name || "")}.json`);
|
|
10933
|
+
await saveToFile(filePath, JSON.stringify(component, null, 2));
|
|
10934
|
+
}
|
|
10935
|
+
const groupsPath = join(componentsDir, DEFAULT_GROUPS_FILENAME);
|
|
10936
|
+
if (resolved.componentFolders.length > 0) {
|
|
10937
|
+
await saveToFile(groupsPath, JSON.stringify(resolved.componentFolders, null, 2));
|
|
10938
|
+
} else if (await fileExists(groupsPath)) {
|
|
10939
|
+
try {
|
|
10940
|
+
await unlink(groupsPath);
|
|
10941
|
+
logger.info("Removed stale local groups file", { path: displayPath(groupsPath, basePath) });
|
|
10942
|
+
} catch (error) {
|
|
10943
|
+
if (error.code !== "ENOENT") {
|
|
10944
|
+
throw error;
|
|
10945
|
+
}
|
|
10946
|
+
}
|
|
10947
|
+
}
|
|
10948
|
+
if (deleteRemoved) {
|
|
10949
|
+
const staleComponents = diffResult.diffs.filter(
|
|
10950
|
+
(d) => d.type === "component" && d.action === "stale"
|
|
10951
|
+
);
|
|
10952
|
+
for (const stale of staleComponents) {
|
|
10953
|
+
const filePath = join(componentsDir, `${sanitizeFilename(stale.name)}.json`);
|
|
10954
|
+
try {
|
|
10955
|
+
await unlink(filePath);
|
|
10956
|
+
logger.info("Removed stale local component file", { path: displayPath(filePath, basePath) });
|
|
10957
|
+
} catch (error) {
|
|
10958
|
+
if (error.code !== "ENOENT") {
|
|
10959
|
+
throw error;
|
|
10960
|
+
}
|
|
10961
|
+
}
|
|
10962
|
+
}
|
|
10963
|
+
}
|
|
10964
|
+
logger.info("Wrote local component files", {
|
|
10965
|
+
space,
|
|
10966
|
+
componentsWritten: resolved.components.length,
|
|
10967
|
+
groupsWritten: resolved.componentFolders.length
|
|
10968
|
+
});
|
|
10969
|
+
}
|
|
10970
|
+
|
|
10971
|
+
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) => {
|
|
10972
|
+
const ui = getUI();
|
|
10973
|
+
const logger = getLogger();
|
|
10974
|
+
const reporter = getReporter();
|
|
10975
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
10976
|
+
const { state } = session();
|
|
10977
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Pushing schema...");
|
|
10978
|
+
logger.info("Schema push started", { entryFile, space });
|
|
10979
|
+
if (!requireAuthentication(state, verbose)) {
|
|
10980
|
+
return;
|
|
10981
|
+
}
|
|
10982
|
+
if (!space) {
|
|
10983
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
10984
|
+
return;
|
|
10985
|
+
}
|
|
10986
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
10987
|
+
try {
|
|
10988
|
+
const loadSpinner = ui.createSpinner("Resolving schema...");
|
|
10989
|
+
let local;
|
|
10990
|
+
try {
|
|
10991
|
+
local = await loadSchema(entryFile);
|
|
10992
|
+
} catch (maybeError) {
|
|
10993
|
+
loadSpinner.failed("Failed to resolve schema");
|
|
10994
|
+
handleError(toError(maybeError), verbose);
|
|
10995
|
+
return;
|
|
10996
|
+
}
|
|
10997
|
+
loadSpinner.succeed(`Found: ${local.components.length} components, ${local.componentFolders.length} component folders, ${local.datasources.length} datasources`);
|
|
10998
|
+
const totalLocal = local.components.length + local.componentFolders.length + local.datasources.length;
|
|
10999
|
+
if (totalLocal === 0) {
|
|
11000
|
+
ui.warn("No components, folders, or datasources found in the entry file. Verify the file exports schema definitions.");
|
|
11001
|
+
return;
|
|
11002
|
+
}
|
|
11003
|
+
const remoteSpinner = ui.createSpinner(`Fetching remote state from space ${space}...`);
|
|
11004
|
+
let remoteResult;
|
|
11005
|
+
try {
|
|
11006
|
+
remoteResult = await fetchRemoteSchema(space);
|
|
11007
|
+
} catch (maybeError) {
|
|
11008
|
+
remoteSpinner.failed("Failed to fetch remote schema");
|
|
11009
|
+
handleError(toError(maybeError), verbose);
|
|
11010
|
+
return;
|
|
11011
|
+
}
|
|
11012
|
+
const { remote, rawComponents, rawComponentFolders, rawDatasources } = remoteResult;
|
|
11013
|
+
remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
|
|
11014
|
+
const { resolved, pendingFolderAssignments } = resolveFolderReferences(local, remote);
|
|
11015
|
+
const diffResult = diffSchema(resolved, remote);
|
|
11016
|
+
ui.br();
|
|
11017
|
+
ui.log(formatDiffOutput(diffResult, { delete: options.delete }));
|
|
11018
|
+
if (options.migrations) {
|
|
11019
|
+
const breakingChanges = analyzeBreakingChanges(diffResult, resolved, remote);
|
|
11020
|
+
if (breakingChanges.length > 0) {
|
|
11021
|
+
const totalChanges = breakingChanges.reduce((sum, c) => sum + c.changes.length, 0);
|
|
11022
|
+
ui.br();
|
|
11023
|
+
ui.warn(`${totalChanges} breaking change(s) detected in ${breakingChanges.length} component(s).`);
|
|
11024
|
+
ui.info("Generated migrations are scaffolds. Review and adjust them before running `storyblok migrations run`.");
|
|
11025
|
+
if (!options.dryRun) {
|
|
11026
|
+
const explicitMigrations = command.getOptionValueSource("migrations") === "cli";
|
|
11027
|
+
const shouldGenerate = explicitMigrations || await confirm({
|
|
11028
|
+
message: "Generate migration files for breaking changes?",
|
|
11029
|
+
default: true
|
|
11030
|
+
});
|
|
11031
|
+
if (shouldGenerate) {
|
|
11032
|
+
const migrationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11033
|
+
const resolvedBase = resolvePath(basePath, "");
|
|
11034
|
+
for (const comp of breakingChanges) {
|
|
11035
|
+
const renames = comp.changes.filter((c) => c.kind === "rename");
|
|
11036
|
+
if (renames.length > 0 && explicitMigrations) {
|
|
11037
|
+
for (const r of renames) {
|
|
11038
|
+
if (r.kind === "rename") {
|
|
11039
|
+
ui.log(` Assumed rename in '${comp.componentName}': ${r.oldField} \u2192 ${r.field}`);
|
|
11040
|
+
}
|
|
11041
|
+
}
|
|
11042
|
+
}
|
|
11043
|
+
if (renames.length > 0 && !explicitMigrations) {
|
|
11044
|
+
ui.br();
|
|
11045
|
+
ui.log(`Detected renames in '${comp.componentName}':`);
|
|
11046
|
+
for (const r of renames) {
|
|
11047
|
+
if (r.kind === "rename") {
|
|
11048
|
+
ui.log(` ${r.oldField} \u2192 ${r.field}`);
|
|
11049
|
+
}
|
|
11050
|
+
}
|
|
11051
|
+
const renameConfirmed = await confirm({
|
|
11052
|
+
message: "Are these renames correct?",
|
|
11053
|
+
default: true
|
|
11054
|
+
});
|
|
11055
|
+
if (!renameConfirmed) {
|
|
11056
|
+
comp.changes = comp.changes.map((c) => {
|
|
11057
|
+
if (c.kind === "rename") {
|
|
11058
|
+
return { kind: "removed", field: c.oldField, renameHint: { newField: c.field } };
|
|
11059
|
+
}
|
|
11060
|
+
return c;
|
|
11061
|
+
});
|
|
11062
|
+
}
|
|
11063
|
+
}
|
|
11064
|
+
const code = renderMigrationCode(comp.changes);
|
|
11065
|
+
const path = await writeMigrationFile({
|
|
11066
|
+
spaceId: space,
|
|
11067
|
+
componentName: comp.componentName,
|
|
11068
|
+
code,
|
|
11069
|
+
timestamp: migrationTimestamp,
|
|
11070
|
+
basePath: resolvedBase
|
|
11071
|
+
});
|
|
11072
|
+
const migrationPath = displayPath(path, basePath);
|
|
11073
|
+
logger.info("Migration generated", { component: comp.componentName, path: migrationPath });
|
|
11074
|
+
ui.log(` Generated: ${migrationPath}`);
|
|
11075
|
+
}
|
|
11076
|
+
ui.br();
|
|
11077
|
+
ui.info(`Run migrations when ready: storyblok migrations run --space ${space}`);
|
|
11078
|
+
}
|
|
11079
|
+
}
|
|
11080
|
+
}
|
|
11081
|
+
}
|
|
11082
|
+
if (options.delete) {
|
|
11083
|
+
const deletedComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
|
|
11084
|
+
for (const comp of deletedComponents) {
|
|
11085
|
+
ui.warn(`Component '${comp.name}' will be deleted. Stories using it will have out-of-schema content.`);
|
|
11086
|
+
}
|
|
11087
|
+
}
|
|
11088
|
+
if (diffResult.stale > 0 && !options.delete) {
|
|
11089
|
+
ui.warn(`${diffResult.stale} stale entity(s) exist remotely but not in schema. Use --delete to remove.`);
|
|
11090
|
+
}
|
|
11091
|
+
if (options.dryRun) {
|
|
11092
|
+
ui.info("Dry run \u2014 no changes applied.");
|
|
11093
|
+
logger.info("Dry run completed", { creates: diffResult.creates, updates: diffResult.updates });
|
|
11094
|
+
return;
|
|
11095
|
+
}
|
|
11096
|
+
const resolvedPath = resolvePath(basePath, "");
|
|
11097
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11098
|
+
const changesetPath = await saveChangeset(resolvedPath, {
|
|
11099
|
+
timestamp,
|
|
11100
|
+
spaceId: Number(space),
|
|
11101
|
+
remote: { components: rawComponents, componentFolders: rawComponentFolders, datasources: rawDatasources },
|
|
11102
|
+
changes: buildChangesetEntries(diffResult, resolved, remote, { delete: options.delete })
|
|
11103
|
+
});
|
|
11104
|
+
logger.info("Changeset saved", { path: displayPath(changesetPath, basePath) });
|
|
11105
|
+
const nothingToPush = diffResult.creates === 0 && diffResult.updates === 0 && (!options.delete || diffResult.stale === 0);
|
|
11106
|
+
if (nothingToPush) {
|
|
11107
|
+
ui.ok("Everything up to date \u2014 nothing to push.");
|
|
11108
|
+
} else {
|
|
11109
|
+
const pushSpinner = ui.createSpinner("Pushing schema...");
|
|
11110
|
+
let result;
|
|
11111
|
+
try {
|
|
11112
|
+
result = await executePush(space, resolved, remote, diffResult, { delete: options.delete, pendingFolderAssignments });
|
|
11113
|
+
} catch (error) {
|
|
11114
|
+
pushSpinner.failed("Failed to push schema");
|
|
11115
|
+
throw error;
|
|
11116
|
+
}
|
|
11117
|
+
summary.total = result.created + result.updated + result.deleted;
|
|
11118
|
+
summary.succeeded = summary.total;
|
|
11119
|
+
pushSpinner.succeed(`Pushed ${result.created} creations, ${result.updated} updates${result.deleted > 0 ? `, ${result.deleted} deletions` : ""}.`);
|
|
11120
|
+
}
|
|
11121
|
+
if (options.writeComponents) {
|
|
11122
|
+
try {
|
|
11123
|
+
await writeLocalComponents({
|
|
11124
|
+
space,
|
|
11125
|
+
basePath,
|
|
11126
|
+
resolved,
|
|
11127
|
+
diffResult,
|
|
11128
|
+
deleteRemoved: options.delete,
|
|
11129
|
+
ui,
|
|
11130
|
+
logger
|
|
11131
|
+
});
|
|
11132
|
+
} catch (writeError) {
|
|
11133
|
+
ui.warn(`Failed to write local component files: ${toError(writeError).message}`);
|
|
11134
|
+
logger.warn("Failed to write local component files", { error: toError(writeError).message });
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
11137
|
+
} catch (maybeError) {
|
|
11138
|
+
summary.failed += 1;
|
|
11139
|
+
handleError(toError(maybeError), verbose);
|
|
11140
|
+
} finally {
|
|
11141
|
+
logger.info("Schema push finished", { summary });
|
|
11142
|
+
reporter.addSummary("schemaPushResults", summary);
|
|
11143
|
+
reporter.finalize();
|
|
11144
|
+
}
|
|
11145
|
+
});
|
|
11146
|
+
|
|
11147
|
+
const FIELD_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "pos"]);
|
|
11148
|
+
function toCamelCase(str) {
|
|
11149
|
+
return str.toLowerCase().replace(/[\s_-]+(.)/g, (_, char) => char.toUpperCase());
|
|
11150
|
+
}
|
|
11151
|
+
function toKebabCase(str) {
|
|
11152
|
+
return str.replace(/[\s_]+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
11153
|
+
}
|
|
11154
|
+
function componentVarName(name) {
|
|
11155
|
+
return `${toCamelCase(name)}Block`;
|
|
11156
|
+
}
|
|
11157
|
+
function folderVarName(name) {
|
|
11158
|
+
return `${toCamelCase(name)}Folder`;
|
|
11159
|
+
}
|
|
11160
|
+
function datasourceVarName(name) {
|
|
11161
|
+
return `${toCamelCase(name)}Datasource`;
|
|
11162
|
+
}
|
|
11163
|
+
function componentFileName(name) {
|
|
11164
|
+
return toKebabCase(name);
|
|
11165
|
+
}
|
|
11166
|
+
function folderFileName(name) {
|
|
11167
|
+
return toKebabCase(name);
|
|
11168
|
+
}
|
|
11169
|
+
function datasourceFileName(datasource) {
|
|
11170
|
+
return toKebabCase(datasource.slug || datasource.name);
|
|
11171
|
+
}
|
|
11172
|
+
function generateFieldCode(fieldName, fieldData, depth) {
|
|
11173
|
+
const clean = stripKeys(fieldData, FIELD_STRIP_KEYS);
|
|
11174
|
+
return `defineField('${fieldName.replace(/'/g, "\\'")}', ${formatValue(clean, depth + 1)})`;
|
|
11175
|
+
}
|
|
11176
|
+
function sortSchemaByPos(schema) {
|
|
11177
|
+
return Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
|
|
11178
|
+
const posA = typeof a.pos === "number" ? a.pos : Infinity;
|
|
11179
|
+
const posB = typeof b.pos === "number" ? b.pos : Infinity;
|
|
11180
|
+
return posA - posB;
|
|
11181
|
+
});
|
|
11182
|
+
}
|
|
11183
|
+
function generateComponentFile(component, componentFolders) {
|
|
11184
|
+
const lines = [];
|
|
11185
|
+
let matchedFolder;
|
|
11186
|
+
if (component.component_group_uuid && componentFolders) {
|
|
11187
|
+
matchedFolder = componentFolders.find((f) => f.uuid === component.component_group_uuid);
|
|
11188
|
+
}
|
|
11189
|
+
lines.push("import {");
|
|
11190
|
+
lines.push(" defineBlock,");
|
|
11191
|
+
lines.push(" defineField,");
|
|
11192
|
+
lines.push("} from '@storyblok/schema';");
|
|
11193
|
+
if (matchedFolder) {
|
|
11194
|
+
lines.push("");
|
|
11195
|
+
const fVarName = folderVarName(matchedFolder.name);
|
|
11196
|
+
const fFileName = folderFileName(matchedFolder.name);
|
|
11197
|
+
lines.push(`import { ${fVarName} } from './folders/${fFileName}';`);
|
|
11198
|
+
}
|
|
11199
|
+
lines.push("");
|
|
11200
|
+
const varName = componentVarName(component.name);
|
|
11201
|
+
lines.push(`export const ${varName} = defineBlock({`);
|
|
11202
|
+
const clean = stripKeys(component, COMPONENT_STRIP_KEYS);
|
|
11203
|
+
if (matchedFolder) {
|
|
11204
|
+
delete clean.component_group_uuid;
|
|
11205
|
+
}
|
|
11206
|
+
const orderedKeys = [];
|
|
11207
|
+
if (clean.name !== void 0) {
|
|
11208
|
+
orderedKeys.push("name");
|
|
11209
|
+
}
|
|
11210
|
+
if (clean.display_name !== void 0) {
|
|
11211
|
+
orderedKeys.push("display_name");
|
|
11212
|
+
}
|
|
11213
|
+
if (clean.is_root !== void 0) {
|
|
11214
|
+
orderedKeys.push("is_root");
|
|
11215
|
+
}
|
|
11216
|
+
if (clean.is_nestable !== void 0) {
|
|
11217
|
+
orderedKeys.push("is_nestable");
|
|
11218
|
+
}
|
|
11219
|
+
const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
|
|
11220
|
+
for (const key of Object.keys(clean).sort()) {
|
|
11221
|
+
if (!handled.has(key)) {
|
|
11222
|
+
orderedKeys.push(key);
|
|
11223
|
+
}
|
|
11224
|
+
}
|
|
11225
|
+
if (matchedFolder) {
|
|
11226
|
+
const fVarName = folderVarName(matchedFolder.name);
|
|
11227
|
+
lines.push(`${INDENT}component_group_uuid: ${fVarName}.uuid,`);
|
|
11228
|
+
}
|
|
11229
|
+
for (const key of orderedKeys) {
|
|
11230
|
+
lines.push(`${INDENT}${key}: ${formatValue(clean[key], 1)},`);
|
|
11231
|
+
}
|
|
11232
|
+
if (clean.schema && typeof clean.schema === "object") {
|
|
11233
|
+
const schema = clean.schema;
|
|
11234
|
+
const sortedFields = sortSchemaByPos(schema);
|
|
11235
|
+
if (sortedFields.length > 0) {
|
|
11236
|
+
lines.push(`${INDENT}schema: [`);
|
|
11237
|
+
for (const [fieldName, fieldData] of sortedFields) {
|
|
11238
|
+
const fieldCode = generateFieldCode(fieldName, fieldData, 2);
|
|
11239
|
+
lines.push(`${INDENT}${INDENT}${fieldCode},`);
|
|
11240
|
+
}
|
|
11241
|
+
lines.push(`${INDENT}],`);
|
|
11242
|
+
} else {
|
|
11243
|
+
lines.push(`${INDENT}schema: [],`);
|
|
11244
|
+
}
|
|
11245
|
+
}
|
|
11246
|
+
lines.push("});");
|
|
11247
|
+
lines.push("");
|
|
11248
|
+
return lines.join("\n");
|
|
11249
|
+
}
|
|
11250
|
+
function generateFolderFile(folder) {
|
|
11251
|
+
const lines = [];
|
|
11252
|
+
lines.push("import { defineBlockFolder } from '@storyblok/schema';");
|
|
11253
|
+
lines.push("");
|
|
11254
|
+
const varName = folderVarName(folder.name);
|
|
11255
|
+
lines.push(`export const ${varName} = defineBlockFolder({`);
|
|
11256
|
+
const clean = stripKeys(folder, FOLDER_INIT_STRIP_KEYS);
|
|
11257
|
+
if (clean.name !== void 0) {
|
|
11258
|
+
lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
|
|
11259
|
+
}
|
|
11260
|
+
const handled = /* @__PURE__ */ new Set(["name"]);
|
|
11261
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
11262
|
+
if (!handled.has(key)) {
|
|
11263
|
+
lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
|
|
11264
|
+
}
|
|
11265
|
+
}
|
|
11266
|
+
lines.push("});");
|
|
11267
|
+
lines.push("");
|
|
11268
|
+
return lines.join("\n");
|
|
11269
|
+
}
|
|
11270
|
+
function generateDatasourceFile(datasource) {
|
|
11271
|
+
const lines = [];
|
|
11272
|
+
lines.push("import { defineDatasource } from '@storyblok/schema';");
|
|
11273
|
+
lines.push("");
|
|
11274
|
+
const varName = datasourceVarName(datasource.name);
|
|
11275
|
+
lines.push(`export const ${varName} = defineDatasource({`);
|
|
11276
|
+
const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
|
|
11277
|
+
if (clean.name !== void 0) {
|
|
11278
|
+
lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
|
|
11279
|
+
}
|
|
11280
|
+
if (clean.slug !== void 0) {
|
|
11281
|
+
lines.push(`${INDENT}slug: ${formatValue(clean.slug, 1)},`);
|
|
11282
|
+
}
|
|
11283
|
+
const handled = /* @__PURE__ */ new Set(["name", "slug"]);
|
|
11284
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
11285
|
+
if (!handled.has(key)) {
|
|
11286
|
+
lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
|
|
11287
|
+
}
|
|
11288
|
+
}
|
|
11289
|
+
lines.push("});");
|
|
11290
|
+
lines.push("");
|
|
11291
|
+
return lines.join("\n");
|
|
11292
|
+
}
|
|
11293
|
+
function generateSchemaFile(components, componentFolders, datasources) {
|
|
11294
|
+
const lines = [];
|
|
11295
|
+
lines.push("import type { Schema as InferSchema, Story as InferStory } from '@storyblok/schema';");
|
|
11296
|
+
lines.push("import type { MapiStory as InferStoryMapi } from '@storyblok/schema';");
|
|
11297
|
+
lines.push("");
|
|
11298
|
+
for (const component of components) {
|
|
11299
|
+
const varName = componentVarName(component.name);
|
|
11300
|
+
const fileName = componentFileName(component.name);
|
|
11301
|
+
lines.push(`import { ${varName} } from './components/${fileName}';`);
|
|
11302
|
+
}
|
|
11303
|
+
for (const folder of componentFolders) {
|
|
11304
|
+
const varName = folderVarName(folder.name);
|
|
11305
|
+
const fileName = folderFileName(folder.name);
|
|
11306
|
+
lines.push(`import { ${varName} } from './components/folders/${fileName}';`);
|
|
11307
|
+
}
|
|
11308
|
+
for (const datasource of datasources) {
|
|
11309
|
+
const varName = datasourceVarName(datasource.name);
|
|
11310
|
+
const fileName = datasourceFileName(datasource);
|
|
11311
|
+
lines.push(`import { ${varName} } from './datasources/${fileName}';`);
|
|
11312
|
+
}
|
|
11313
|
+
lines.push("");
|
|
11314
|
+
lines.push("export const schema = {");
|
|
11315
|
+
if (components.length > 0) {
|
|
11316
|
+
lines.push(" blocks: {");
|
|
11317
|
+
for (const component of components) {
|
|
11318
|
+
const varName = componentVarName(component.name);
|
|
11319
|
+
lines.push(` ${varName},`);
|
|
11320
|
+
}
|
|
11321
|
+
lines.push(" },");
|
|
11322
|
+
}
|
|
11323
|
+
if (componentFolders.length > 0) {
|
|
11324
|
+
lines.push(" blockFolders: {");
|
|
11325
|
+
for (const folder of componentFolders) {
|
|
11326
|
+
const varName = folderVarName(folder.name);
|
|
11327
|
+
lines.push(` ${varName},`);
|
|
11328
|
+
}
|
|
11329
|
+
lines.push(" },");
|
|
11330
|
+
}
|
|
11331
|
+
if (datasources.length > 0) {
|
|
11332
|
+
lines.push(" datasources: {");
|
|
11333
|
+
for (const datasource of datasources) {
|
|
11334
|
+
const varName = datasourceVarName(datasource.name);
|
|
11335
|
+
lines.push(` ${varName},`);
|
|
11336
|
+
}
|
|
11337
|
+
lines.push(" },");
|
|
11338
|
+
}
|
|
11339
|
+
lines.push("};");
|
|
11340
|
+
lines.push("");
|
|
11341
|
+
lines.push("export type Schema = InferSchema<typeof schema>;");
|
|
11342
|
+
lines.push("export type Blocks = Schema['blocks'];");
|
|
11343
|
+
lines.push("export type Story = InferStory<Blocks>;");
|
|
11344
|
+
lines.push("export type StoryMapi = InferStoryMapi<Blocks>;");
|
|
11345
|
+
lines.push("");
|
|
11346
|
+
return lines.join("\n");
|
|
11347
|
+
}
|
|
11348
|
+
|
|
11349
|
+
async function writeFileWithDirs(filePath, content) {
|
|
11350
|
+
const dir = dirname(filePath);
|
|
11351
|
+
await mkdir(dir, { recursive: true });
|
|
11352
|
+
await writeFile(filePath, content, "utf-8");
|
|
11353
|
+
}
|
|
11354
|
+
async function writeSchemaFiles(targetPath, components, componentFolders, datasources) {
|
|
11355
|
+
const writtenFiles = [];
|
|
11356
|
+
for (const comp of components) {
|
|
11357
|
+
const fileName = componentFileName(comp.name);
|
|
11358
|
+
const filePath = join(targetPath, "components", `${fileName}.ts`);
|
|
11359
|
+
await writeFileWithDirs(filePath, generateComponentFile(comp, componentFolders));
|
|
11360
|
+
writtenFiles.push(filePath);
|
|
11361
|
+
}
|
|
11362
|
+
for (const folder of componentFolders) {
|
|
11363
|
+
const fileName = folderFileName(folder.name);
|
|
11364
|
+
const filePath = join(targetPath, "components", "folders", `${fileName}.ts`);
|
|
11365
|
+
await writeFileWithDirs(filePath, generateFolderFile(folder));
|
|
11366
|
+
writtenFiles.push(filePath);
|
|
11367
|
+
}
|
|
11368
|
+
for (const ds of datasources) {
|
|
11369
|
+
const fileName = datasourceFileName(ds);
|
|
11370
|
+
const filePath = join(targetPath, "datasources", `${fileName}.ts`);
|
|
11371
|
+
await writeFileWithDirs(filePath, generateDatasourceFile(ds));
|
|
11372
|
+
writtenFiles.push(filePath);
|
|
11373
|
+
}
|
|
11374
|
+
const schemaPath = join(targetPath, "schema.ts");
|
|
11375
|
+
await writeFileWithDirs(schemaPath, generateSchemaFile(components, componentFolders, datasources));
|
|
11376
|
+
writtenFiles.push(schemaPath);
|
|
11377
|
+
return writtenFiles;
|
|
11378
|
+
}
|
|
11379
|
+
|
|
11380
|
+
async function isTargetEmpty(targetPath) {
|
|
11381
|
+
try {
|
|
11382
|
+
const entries = await readdir(targetPath);
|
|
11383
|
+
return entries.every((entry) => entry.startsWith("."));
|
|
11384
|
+
} catch (maybeError) {
|
|
11385
|
+
const error = maybeError;
|
|
11386
|
+
if (error?.code === "ENOENT") {
|
|
11387
|
+
return true;
|
|
11388
|
+
}
|
|
11389
|
+
throw error;
|
|
11390
|
+
}
|
|
11391
|
+
}
|
|
11392
|
+
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) => {
|
|
11393
|
+
const ui = getUI();
|
|
11394
|
+
const logger = getLogger();
|
|
11395
|
+
const reporter = getReporter();
|
|
11396
|
+
const { space, verbose } = command.optsWithGlobals();
|
|
11397
|
+
const { state } = session();
|
|
11398
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Initializing schema...");
|
|
11399
|
+
logger.info("Schema init started", { space });
|
|
11400
|
+
if (!requireAuthentication(state, verbose)) {
|
|
11401
|
+
return;
|
|
11402
|
+
}
|
|
11403
|
+
if (!space) {
|
|
11404
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
11405
|
+
return;
|
|
11406
|
+
}
|
|
11407
|
+
const targetPath = resolve(options.outDir);
|
|
11408
|
+
const targetDisplayPath = displayPath(targetPath, options.outDir);
|
|
11409
|
+
if (!await isTargetEmpty(targetPath)) {
|
|
11410
|
+
handleError(
|
|
11411
|
+
new CommandError(`Target directory ${targetDisplayPath} 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.`),
|
|
11412
|
+
verbose
|
|
11413
|
+
);
|
|
11414
|
+
return;
|
|
11415
|
+
}
|
|
11416
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
11417
|
+
try {
|
|
11418
|
+
const fetchSpinner = ui.createSpinner(`Fetching schema from space ${space}...`);
|
|
11419
|
+
let fetchResult;
|
|
11420
|
+
try {
|
|
11421
|
+
fetchResult = await fetchRemoteSchema(space);
|
|
11422
|
+
} catch (maybeError) {
|
|
11423
|
+
fetchSpinner.failed("Failed to fetch remote schema");
|
|
11424
|
+
handleError(toError(maybeError), verbose);
|
|
11425
|
+
return;
|
|
11426
|
+
}
|
|
11427
|
+
const { rawComponents, rawComponentFolders, rawDatasources } = fetchResult;
|
|
11428
|
+
fetchSpinner.succeed(`Found: ${rawComponents.length} components, ${rawComponentFolders.length} component folders, ${rawDatasources.length} datasources`);
|
|
11429
|
+
const writeSpinner = ui.createSpinner(`Generating TypeScript files to ${targetDisplayPath}...`);
|
|
11430
|
+
const writtenFiles = await writeSchemaFiles(targetPath, rawComponents, rawComponentFolders, rawDatasources);
|
|
11431
|
+
summary.total = writtenFiles.length;
|
|
11432
|
+
summary.succeeded = writtenFiles.length;
|
|
11433
|
+
writeSpinner.succeed(`Generated ${writtenFiles.length} files`);
|
|
11434
|
+
ui.list(writtenFiles.map((file) => displayPath(file, options.outDir)));
|
|
11435
|
+
ui.warn("`schema init` is a one-time bootstrap step for adopting an existing space. Review generated files before continuing.");
|
|
11436
|
+
ui.info("After bootstrapping, keep your local schema as the source of truth and use `schema push` for ongoing changes.");
|
|
11437
|
+
ui.info("Make sure `@storyblok/schema` is installed in the project that imports these files (e.g. `pnpm add @storyblok/schema`).");
|
|
11438
|
+
} catch (maybeError) {
|
|
11439
|
+
summary.failed += 1;
|
|
11440
|
+
handleError(toError(maybeError), verbose);
|
|
11441
|
+
} finally {
|
|
11442
|
+
logger.info("Schema init finished", { summary });
|
|
11443
|
+
reporter.addSummary("schemaInitResults", summary);
|
|
11444
|
+
reporter.finalize();
|
|
11445
|
+
}
|
|
11446
|
+
});
|
|
11447
|
+
|
|
11448
|
+
const API_ASSIGNED_FIELDS = [
|
|
11449
|
+
"id",
|
|
11450
|
+
"created_at",
|
|
11451
|
+
"updated_at",
|
|
11452
|
+
"real_name",
|
|
11453
|
+
"all_presets",
|
|
11454
|
+
"image",
|
|
11455
|
+
"uuid"
|
|
11456
|
+
];
|
|
11457
|
+
function stripApiFields(payload) {
|
|
11458
|
+
const result = { ...payload };
|
|
11459
|
+
for (const field of API_ASSIGNED_FIELDS) {
|
|
11460
|
+
delete result[field];
|
|
11461
|
+
}
|
|
11462
|
+
return result;
|
|
11463
|
+
}
|
|
11464
|
+
async function listChangesets(basePath) {
|
|
11465
|
+
const dir = join(basePath, "schema", "changesets");
|
|
11466
|
+
if (!await fileExists(dir)) {
|
|
11467
|
+
return [];
|
|
11468
|
+
}
|
|
11469
|
+
const files = await readDirectory(dir);
|
|
11470
|
+
return files.filter((f) => f.endsWith(".json")).sort().reverse().map((f) => join(dir, f));
|
|
11471
|
+
}
|
|
11472
|
+
async function loadChangeset(filePath) {
|
|
11473
|
+
const content = await readFile$1(filePath, "utf-8");
|
|
11474
|
+
return JSON.parse(content);
|
|
11475
|
+
}
|
|
11476
|
+
function buildRollbackOps(changeset) {
|
|
11477
|
+
if (changeset.changes.length === 0) {
|
|
11478
|
+
return [];
|
|
11479
|
+
}
|
|
11480
|
+
return changeset.changes.map((entry) => {
|
|
11481
|
+
switch (entry.action) {
|
|
11482
|
+
case "create":
|
|
11483
|
+
return { type: entry.type, name: entry.name, action: "delete", payload: {} };
|
|
11484
|
+
case "update":
|
|
11485
|
+
return { type: entry.type, name: entry.name, action: "update", payload: entry.before ?? {} };
|
|
11486
|
+
case "delete":
|
|
11487
|
+
return { type: entry.type, name: entry.name, action: "create", payload: entry.before ?? {} };
|
|
11488
|
+
default:
|
|
11489
|
+
return { type: entry.type, name: entry.name, action: entry.action, payload: {} };
|
|
11490
|
+
}
|
|
11491
|
+
});
|
|
11492
|
+
}
|
|
11493
|
+
function rollbackAction(original) {
|
|
11494
|
+
switch (original) {
|
|
11495
|
+
case "create":
|
|
11496
|
+
return "delete";
|
|
11497
|
+
case "update":
|
|
11498
|
+
return "update";
|
|
11499
|
+
case "delete":
|
|
11500
|
+
return "create";
|
|
11501
|
+
}
|
|
11502
|
+
}
|
|
11503
|
+
function formatRollbackOutput(changes) {
|
|
11504
|
+
const byType = {
|
|
11505
|
+
component: [],
|
|
11506
|
+
componentFolder: [],
|
|
11507
|
+
datasource: []
|
|
11508
|
+
};
|
|
11509
|
+
for (const entry of changes) {
|
|
11510
|
+
byType[entry.type]?.push(entry);
|
|
11511
|
+
}
|
|
11512
|
+
const icons = {
|
|
11513
|
+
create: chalk.green("+"),
|
|
11514
|
+
update: chalk.yellow("~"),
|
|
11515
|
+
delete: chalk.red("-")
|
|
11516
|
+
};
|
|
11517
|
+
const lines = [];
|
|
11518
|
+
const sections = [
|
|
11519
|
+
["Components", byType.component],
|
|
11520
|
+
["Component Folders", byType.componentFolder],
|
|
11521
|
+
["Datasources", byType.datasource]
|
|
11522
|
+
];
|
|
11523
|
+
for (const [label, entries] of sections) {
|
|
11524
|
+
if (entries.length === 0) {
|
|
11525
|
+
continue;
|
|
11526
|
+
}
|
|
11527
|
+
lines.push(chalk.bold(label));
|
|
11528
|
+
for (const entry of entries) {
|
|
11529
|
+
const action = rollbackAction(entry.action);
|
|
11530
|
+
const icon = icons[action] ?? " ";
|
|
11531
|
+
const name = action === "delete" ? chalk.red(entry.name) : entry.name;
|
|
11532
|
+
lines.push(` ${icon} ${name} ${chalk.dim(`(${action})`)}`);
|
|
11533
|
+
if (entry.action === "update" && entry.before && entry.after) {
|
|
11534
|
+
let fromStr;
|
|
11535
|
+
let toStr;
|
|
11536
|
+
if (entry.type === "component") {
|
|
11537
|
+
fromStr = serializeComponent(applyDefaults(entry.after, COMPONENT_DEFAULTS));
|
|
11538
|
+
toStr = serializeComponent(applyDefaults(entry.before, COMPONENT_DEFAULTS));
|
|
11539
|
+
} else if (entry.type === "componentFolder") {
|
|
11540
|
+
fromStr = serializeComponentFolder(entry.after);
|
|
11541
|
+
toStr = serializeComponentFolder(entry.before);
|
|
11542
|
+
} else {
|
|
11543
|
+
fromStr = serializeDatasource(entry.after);
|
|
11544
|
+
toStr = serializeDatasource(entry.before);
|
|
11545
|
+
}
|
|
11546
|
+
if (fromStr !== toStr) {
|
|
11547
|
+
const patch = createTwoFilesPatch(
|
|
11548
|
+
`current/${entry.name}`,
|
|
11549
|
+
`restore/${entry.name}`,
|
|
11550
|
+
fromStr,
|
|
11551
|
+
toStr,
|
|
11552
|
+
"current",
|
|
11553
|
+
"restore"
|
|
11554
|
+
);
|
|
11555
|
+
for (const line of patch.split("\n")) {
|
|
11556
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
11557
|
+
lines.push(` ${chalk.green(line)}`);
|
|
11558
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
11559
|
+
lines.push(` ${chalk.red(line)}`);
|
|
11560
|
+
}
|
|
11561
|
+
}
|
|
11562
|
+
}
|
|
11563
|
+
}
|
|
11564
|
+
}
|
|
11565
|
+
lines.push("");
|
|
11566
|
+
}
|
|
11567
|
+
return lines.join("\n").trimEnd();
|
|
11568
|
+
}
|
|
11569
|
+
async function executeRollback(spaceId, ops, remote) {
|
|
11570
|
+
const client = getMapiClient();
|
|
11571
|
+
const spaceIdNum = Number(spaceId);
|
|
11572
|
+
let created = 0;
|
|
11573
|
+
let updated = 0;
|
|
11574
|
+
let deleted = 0;
|
|
11575
|
+
const folderOps = ops.filter((op) => op.type === "componentFolder");
|
|
11576
|
+
const componentOps = ops.filter((op) => op.type === "component");
|
|
11577
|
+
const datasourceOps = ops.filter((op) => op.type === "datasource");
|
|
11578
|
+
const folderUuidRemap = /* @__PURE__ */ new Map();
|
|
11579
|
+
for (const op of folderOps.filter((o) => o.action !== "delete")) {
|
|
11580
|
+
if (op.action === "create") {
|
|
11581
|
+
const oldUuid = op.payload.uuid;
|
|
11582
|
+
const payload = toComponentFolderCreate(stripApiFields(op.payload));
|
|
11583
|
+
try {
|
|
11584
|
+
const response = await client.componentFolders.create({
|
|
11585
|
+
path: { space_id: spaceIdNum },
|
|
11586
|
+
body: { component_group: payload },
|
|
11587
|
+
throwOnError: true
|
|
11588
|
+
});
|
|
11589
|
+
const remoteUuid = response.data?.component_group?.uuid;
|
|
11590
|
+
if (remoteUuid && typeof oldUuid === "string") {
|
|
11591
|
+
folderUuidRemap.set(oldUuid, remoteUuid);
|
|
11592
|
+
}
|
|
11593
|
+
created++;
|
|
11594
|
+
} catch (error) {
|
|
11595
|
+
handleAPIError("push_component_group", error, `Failed to create folder ${op.name}`);
|
|
11596
|
+
}
|
|
11597
|
+
} else if (op.action === "update") {
|
|
11598
|
+
const existing = remote.componentFolders.get(op.name);
|
|
11599
|
+
if (existing?.id) {
|
|
11600
|
+
const payload = toComponentFolderCreate(stripApiFields(op.payload));
|
|
11601
|
+
try {
|
|
11602
|
+
await client.componentFolders.update(existing.id, {
|
|
11603
|
+
path: { space_id: spaceIdNum },
|
|
11604
|
+
body: { component_group: payload },
|
|
11605
|
+
throwOnError: true
|
|
11606
|
+
});
|
|
11607
|
+
updated++;
|
|
11608
|
+
} catch (error) {
|
|
11609
|
+
handleAPIError("update_component_group", error, `Failed to update folder ${op.name}`);
|
|
11610
|
+
}
|
|
11611
|
+
}
|
|
11612
|
+
}
|
|
11613
|
+
}
|
|
11614
|
+
for (const op of componentOps) {
|
|
11615
|
+
const oldUuid = op.payload.component_group_uuid;
|
|
11616
|
+
if (typeof oldUuid !== "string") {
|
|
11617
|
+
continue;
|
|
11618
|
+
}
|
|
11619
|
+
const newUuid = folderUuidRemap.get(oldUuid);
|
|
11620
|
+
if (newUuid) {
|
|
11621
|
+
op.payload.component_group_uuid = newUuid;
|
|
11622
|
+
}
|
|
11623
|
+
}
|
|
11624
|
+
for (const op of componentOps.filter((o) => o.action !== "delete")) {
|
|
11625
|
+
if (op.action === "create") {
|
|
11626
|
+
const payload = toComponentCreate(stripApiFields(op.payload));
|
|
11627
|
+
try {
|
|
11628
|
+
await client.components.create({
|
|
11629
|
+
path: { space_id: spaceIdNum },
|
|
11630
|
+
body: { component: payload },
|
|
11631
|
+
throwOnError: true
|
|
11632
|
+
});
|
|
11633
|
+
created++;
|
|
11634
|
+
} catch (error) {
|
|
11635
|
+
handleAPIError("push_component", error, `Failed to create component ${op.name}`);
|
|
11636
|
+
}
|
|
11637
|
+
} else if (op.action === "update") {
|
|
11638
|
+
const existing = remote.components.get(op.name);
|
|
11639
|
+
if (existing?.id) {
|
|
11640
|
+
const payload = toComponentUpdate(stripApiFields(op.payload));
|
|
11641
|
+
try {
|
|
11642
|
+
await client.components.update(existing.id, {
|
|
11643
|
+
path: { space_id: spaceIdNum },
|
|
11644
|
+
body: { component: payload },
|
|
11645
|
+
throwOnError: true
|
|
11646
|
+
});
|
|
11647
|
+
updated++;
|
|
11648
|
+
} catch (error) {
|
|
11649
|
+
handleAPIError("update_component", error, `Failed to update component ${op.name}`);
|
|
11650
|
+
}
|
|
11651
|
+
}
|
|
11652
|
+
}
|
|
11653
|
+
}
|
|
11654
|
+
for (const op of datasourceOps.filter((o) => o.action !== "delete")) {
|
|
11655
|
+
if (op.action === "create") {
|
|
11656
|
+
const payload = toDatasourceCreate(stripApiFields(op.payload));
|
|
11657
|
+
try {
|
|
11658
|
+
await client.datasources.create({
|
|
11659
|
+
path: { space_id: spaceIdNum },
|
|
11660
|
+
body: { datasource: payload },
|
|
11661
|
+
throwOnError: true
|
|
11662
|
+
});
|
|
11663
|
+
created++;
|
|
11664
|
+
} catch (error) {
|
|
11665
|
+
handleAPIError("push_datasource", error, `Failed to create datasource ${op.name}`);
|
|
11666
|
+
}
|
|
11667
|
+
} else if (op.action === "update") {
|
|
11668
|
+
const existing = remote.datasources.get(op.name);
|
|
11669
|
+
if (existing?.id) {
|
|
11670
|
+
const payload = toDatasourceUpdate(stripApiFields(op.payload), existing);
|
|
11671
|
+
try {
|
|
11672
|
+
await client.datasources.update(existing.id, {
|
|
11673
|
+
path: { space_id: spaceIdNum },
|
|
11674
|
+
body: { datasource: payload },
|
|
11675
|
+
throwOnError: true
|
|
11676
|
+
});
|
|
11677
|
+
updated++;
|
|
11678
|
+
} catch (error) {
|
|
11679
|
+
handleAPIError("update_datasource", error, `Failed to update datasource ${op.name}`);
|
|
11680
|
+
}
|
|
11681
|
+
}
|
|
11682
|
+
}
|
|
11683
|
+
}
|
|
11684
|
+
for (const op of datasourceOps.filter((o) => o.action === "delete")) {
|
|
11685
|
+
const existing = remote.datasources.get(op.name);
|
|
11686
|
+
if (existing?.id) {
|
|
11687
|
+
try {
|
|
11688
|
+
await client.datasources.delete(existing.id, {
|
|
11689
|
+
path: { space_id: spaceIdNum },
|
|
11690
|
+
throwOnError: true
|
|
11691
|
+
});
|
|
11692
|
+
deleted++;
|
|
11693
|
+
} catch (error) {
|
|
11694
|
+
handleAPIError("delete_datasource", error, `Failed to delete datasource ${op.name}`);
|
|
11695
|
+
}
|
|
11696
|
+
}
|
|
11697
|
+
}
|
|
11698
|
+
for (const op of componentOps.filter((o) => o.action === "delete")) {
|
|
11699
|
+
const existing = remote.components.get(op.name);
|
|
11700
|
+
if (existing?.id) {
|
|
11701
|
+
try {
|
|
11702
|
+
await client.components.delete(existing.id, {
|
|
11703
|
+
path: { space_id: spaceIdNum },
|
|
11704
|
+
throwOnError: true
|
|
11705
|
+
});
|
|
11706
|
+
deleted++;
|
|
11707
|
+
} catch (error) {
|
|
11708
|
+
handleAPIError("push_component", error, `Failed to delete component ${op.name}`);
|
|
11709
|
+
}
|
|
11710
|
+
}
|
|
11711
|
+
}
|
|
11712
|
+
for (const op of folderOps.filter((o) => o.action === "delete")) {
|
|
11713
|
+
const existing = remote.componentFolders.get(op.name);
|
|
11714
|
+
if (existing?.id) {
|
|
11715
|
+
try {
|
|
11716
|
+
await client.componentFolders.delete(existing.id, {
|
|
11717
|
+
path: { space_id: spaceIdNum },
|
|
11718
|
+
throwOnError: true
|
|
11719
|
+
});
|
|
11720
|
+
deleted++;
|
|
11721
|
+
} catch (error) {
|
|
11722
|
+
handleAPIError("push_component_group", error, `Failed to delete folder ${op.name}`);
|
|
11723
|
+
}
|
|
11724
|
+
}
|
|
11725
|
+
}
|
|
11726
|
+
return { created, updated, deleted };
|
|
11727
|
+
}
|
|
11728
|
+
|
|
11729
|
+
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) => {
|
|
11730
|
+
const ui = getUI();
|
|
11731
|
+
const logger = getLogger();
|
|
11732
|
+
const reporter = getReporter();
|
|
11733
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
11734
|
+
const { state } = session();
|
|
11735
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Rolling back schema...");
|
|
11736
|
+
logger.info("Schema rollback started", { changesetFile, space });
|
|
11737
|
+
if (!requireAuthentication(state, verbose)) {
|
|
11738
|
+
return;
|
|
11739
|
+
}
|
|
11740
|
+
if (!space) {
|
|
11741
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
11742
|
+
return;
|
|
11743
|
+
}
|
|
11744
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
11745
|
+
try {
|
|
11746
|
+
const resolvedBase = resolvePath(basePath, "");
|
|
11747
|
+
let resolvedFile;
|
|
11748
|
+
if (changesetFile) {
|
|
11749
|
+
resolvedFile = changesetFile;
|
|
11750
|
+
} else if (options.latest) {
|
|
11751
|
+
const available = await listChangesets(resolvedBase);
|
|
11752
|
+
if (available.length === 0) {
|
|
11753
|
+
ui.warn("No changesets found. Run `schema push` first to create one.");
|
|
11754
|
+
return;
|
|
11755
|
+
}
|
|
11756
|
+
resolvedFile = available[0];
|
|
11757
|
+
ui.info(`Using latest changeset: ${basename(resolvedFile)}`);
|
|
11758
|
+
} else {
|
|
11759
|
+
const available = await listChangesets(resolvedBase);
|
|
11760
|
+
if (available.length === 0) {
|
|
11761
|
+
ui.warn("No changesets found. Run `schema push` first to create one.");
|
|
11762
|
+
return;
|
|
11763
|
+
}
|
|
11764
|
+
resolvedFile = await select({
|
|
11765
|
+
message: "Select a changeset to roll back:",
|
|
11766
|
+
choices: available.map((f) => ({ name: basename(f), value: f }))
|
|
11767
|
+
});
|
|
11768
|
+
}
|
|
11769
|
+
let changeset;
|
|
11770
|
+
try {
|
|
11771
|
+
changeset = await loadChangeset(resolvedFile);
|
|
11772
|
+
} catch (maybeError) {
|
|
11773
|
+
handleError(toError(maybeError), verbose);
|
|
11774
|
+
return;
|
|
11775
|
+
}
|
|
11776
|
+
logger.info("Changeset loaded", { file: resolvedFile, changes: changeset.changes.length });
|
|
11777
|
+
const ops = buildRollbackOps(changeset);
|
|
11778
|
+
if (ops.length === 0) {
|
|
11779
|
+
ui.ok("Changeset has no changes \u2014 nothing to roll back.");
|
|
11780
|
+
return;
|
|
11781
|
+
}
|
|
11782
|
+
ui.br();
|
|
11783
|
+
ui.log(formatRollbackOutput(changeset.changes));
|
|
11784
|
+
if (options.dryRun) {
|
|
11785
|
+
ui.info("Dry run \u2014 no changes applied.");
|
|
11786
|
+
logger.info("Dry run completed", { ops: ops.length });
|
|
11787
|
+
return;
|
|
11788
|
+
}
|
|
11789
|
+
if (!options.yes) {
|
|
11790
|
+
const confirmed = await confirm({
|
|
11791
|
+
message: `Apply rollback of ${ops.length} change(s) from ${basename(resolvedFile)}?`,
|
|
11792
|
+
default: false
|
|
11793
|
+
});
|
|
11794
|
+
if (!confirmed) {
|
|
11795
|
+
ui.info("Rollback cancelled.");
|
|
11796
|
+
return;
|
|
11797
|
+
}
|
|
11798
|
+
}
|
|
11799
|
+
const remoteSpinner = ui.createSpinner(`Fetching current remote state from space ${space}...`);
|
|
11800
|
+
let remoteResult;
|
|
11801
|
+
try {
|
|
11802
|
+
remoteResult = await fetchRemoteSchema(space);
|
|
11803
|
+
} catch (maybeError) {
|
|
11804
|
+
remoteSpinner.failed("Failed to fetch remote schema");
|
|
11805
|
+
handleError(toError(maybeError), verbose);
|
|
11806
|
+
return;
|
|
11807
|
+
}
|
|
11808
|
+
const { remote } = remoteResult;
|
|
11809
|
+
remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
|
|
11810
|
+
const rollbackSpinner = ui.createSpinner("Applying rollback...");
|
|
11811
|
+
let result;
|
|
11812
|
+
try {
|
|
11813
|
+
result = await executeRollback(space, ops, remote);
|
|
11814
|
+
} catch (error) {
|
|
11815
|
+
rollbackSpinner.failed("Failed to apply rollback");
|
|
11816
|
+
throw error;
|
|
11817
|
+
}
|
|
11818
|
+
summary.total = result.created + result.updated + result.deleted;
|
|
11819
|
+
summary.succeeded = summary.total;
|
|
11820
|
+
rollbackSpinner.succeed(`Rolled back: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted.`);
|
|
11821
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11822
|
+
const rollbackChangesetPath = await saveChangeset(resolvedBase, {
|
|
11823
|
+
timestamp,
|
|
11824
|
+
spaceId: Number(space),
|
|
11825
|
+
remote: { components: remoteResult.rawComponents, componentFolders: remoteResult.rawComponentFolders, datasources: remoteResult.rawDatasources },
|
|
11826
|
+
changes: ops.map((op) => ({
|
|
11827
|
+
type: op.type,
|
|
11828
|
+
name: op.name,
|
|
11829
|
+
action: op.action,
|
|
11830
|
+
...Object.keys(op.payload).length > 0 && { after: op.payload }
|
|
11831
|
+
}))
|
|
11832
|
+
});
|
|
11833
|
+
logger.info("Rollback changeset saved", { path: displayPath(rollbackChangesetPath, basePath) });
|
|
11834
|
+
} catch (maybeError) {
|
|
11835
|
+
summary.failed += 1;
|
|
11836
|
+
handleError(toError(maybeError), verbose);
|
|
11837
|
+
} finally {
|
|
11838
|
+
logger.info("Schema rollback finished", { summary });
|
|
11839
|
+
reporter.addSummary("schemaRollbackResults", summary);
|
|
11840
|
+
reporter.finalize();
|
|
11841
|
+
}
|
|
11842
|
+
});
|
|
11843
|
+
|
|
9785
11844
|
const program = getProgram();
|
|
9786
11845
|
konsola.br();
|
|
9787
11846
|
konsola.br();
|