storyblok 4.19.0 → 4.20.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 +2065 -153
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
package/dist/index.mjs
CHANGED
|
@@ -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",
|
|
@@ -702,6 +705,7 @@ const API_ACTIONS = {
|
|
|
702
705
|
update_component_group: "Failed to update component group",
|
|
703
706
|
update_component_preset: "Failed to update component preset",
|
|
704
707
|
delete_component_preset: "Failed to delete component preset",
|
|
708
|
+
delete_component: "Failed to delete component",
|
|
705
709
|
pull_stories: "Failed to pull stories",
|
|
706
710
|
pull_story: "Failed to pull story",
|
|
707
711
|
create_story: "Failed to create story",
|
|
@@ -729,6 +733,8 @@ const API_ACTIONS = {
|
|
|
729
733
|
push_datasource: "Failed to push datasource",
|
|
730
734
|
update_datasource: "Failed to update datasource",
|
|
731
735
|
delete_datasource: "Failed to delete datasource",
|
|
736
|
+
push_datasource_entry: "Failed to push datasource entry",
|
|
737
|
+
update_datasource_entry: "Failed to update datasource entry",
|
|
732
738
|
delete_datasource_entry: "Failed to delete datasource entry",
|
|
733
739
|
create_space: "Failed to create space",
|
|
734
740
|
pull_spaces: "Failed to pull spaces",
|
|
@@ -1078,6 +1084,7 @@ function maskToken(token) {
|
|
|
1078
1084
|
const maskedPart = "*".repeat(token.length - 4);
|
|
1079
1085
|
return `${visiblePart}${maskedPart}`;
|
|
1080
1086
|
}
|
|
1087
|
+
const slugify = (text) => text.toString().toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]+/g, "").replace(/-{2,}/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
1081
1088
|
function createRegexFromGlob(pattern) {
|
|
1082
1089
|
return new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*")}$`);
|
|
1083
1090
|
}
|
|
@@ -1159,6 +1166,25 @@ function getPackageJson() {
|
|
|
1159
1166
|
return packageJson$1;
|
|
1160
1167
|
}
|
|
1161
1168
|
|
|
1169
|
+
async function fetchAllPages(fetchFunction, extractDataFunction) {
|
|
1170
|
+
const items = [];
|
|
1171
|
+
let page = 1;
|
|
1172
|
+
while (true) {
|
|
1173
|
+
const { data, response } = await fetchFunction(page);
|
|
1174
|
+
const totalHeader = response.headers.get("total");
|
|
1175
|
+
const fetchedItems = extractDataFunction(data);
|
|
1176
|
+
items.push(...fetchedItems);
|
|
1177
|
+
if (!totalHeader) {
|
|
1178
|
+
return items;
|
|
1179
|
+
}
|
|
1180
|
+
const total = Number(totalHeader);
|
|
1181
|
+
if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
|
|
1182
|
+
return items;
|
|
1183
|
+
}
|
|
1184
|
+
page++;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1162
1188
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
1163
1189
|
const __dirname$1 = dirname(__filename$1);
|
|
1164
1190
|
function isRegion(value) {
|
|
@@ -1247,6 +1273,10 @@ class UI {
|
|
|
1247
1273
|
this.br();
|
|
1248
1274
|
}
|
|
1249
1275
|
}
|
|
1276
|
+
/** Plain console.log passthrough — use for preformatted or multi-line text. */
|
|
1277
|
+
log(message) {
|
|
1278
|
+
this.console?.log(message);
|
|
1279
|
+
}
|
|
1250
1280
|
list(items) {
|
|
1251
1281
|
for (const item of items) {
|
|
1252
1282
|
this.console?.log(` ${item}`);
|
|
@@ -2164,14 +2194,14 @@ async function performInteractiveLogin(options) {
|
|
|
2164
2194
|
}
|
|
2165
2195
|
}
|
|
2166
2196
|
|
|
2167
|
-
const program$
|
|
2197
|
+
const program$f = getProgram();
|
|
2168
2198
|
const allRegionsText = Object.values(regions).join(",");
|
|
2169
|
-
program$
|
|
2199
|
+
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(
|
|
2170
2200
|
"-r, --region <region>",
|
|
2171
2201
|
`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}.`
|
|
2172
2202
|
).action(async (options) => {
|
|
2173
2203
|
konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
|
|
2174
|
-
const verbose = program$
|
|
2204
|
+
const verbose = program$f.opts().verbose;
|
|
2175
2205
|
const { token, region } = options;
|
|
2176
2206
|
const { state, updateSession, persistCredentials } = session();
|
|
2177
2207
|
if (state.isLoggedIn && !state.envLogin) {
|
|
@@ -2229,10 +2259,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
|
|
|
2229
2259
|
konsola.br();
|
|
2230
2260
|
});
|
|
2231
2261
|
|
|
2232
|
-
const program$
|
|
2233
|
-
program$
|
|
2262
|
+
const program$e = getProgram();
|
|
2263
|
+
program$e.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
|
|
2234
2264
|
konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
|
|
2235
|
-
const verbose = program$
|
|
2265
|
+
const verbose = program$e.opts().verbose;
|
|
2236
2266
|
try {
|
|
2237
2267
|
const { state } = session();
|
|
2238
2268
|
if (!state.isLoggedIn || !state.password || !state.region) {
|
|
@@ -2279,10 +2309,10 @@ async function openSignupInBrowser(url) {
|
|
|
2279
2309
|
}
|
|
2280
2310
|
}
|
|
2281
2311
|
|
|
2282
|
-
const program$
|
|
2283
|
-
program$
|
|
2312
|
+
const program$d = getProgram();
|
|
2313
|
+
program$d.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
|
|
2284
2314
|
konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
|
|
2285
|
-
const verbose = program$
|
|
2315
|
+
const verbose = program$d.opts().verbose;
|
|
2286
2316
|
const { state } = session();
|
|
2287
2317
|
if (state.isLoggedIn && !state.envLogin) {
|
|
2288
2318
|
konsola.ok(`You are already logged in. If you want to signup with a different account, please logout first.`);
|
|
@@ -2303,10 +2333,10 @@ program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
|
|
|
2303
2333
|
konsola.br();
|
|
2304
2334
|
});
|
|
2305
2335
|
|
|
2306
|
-
const program$
|
|
2307
|
-
program$
|
|
2336
|
+
const program$c = getProgram();
|
|
2337
|
+
program$c.command(commands.USER).description("Get the current user").action(async () => {
|
|
2308
2338
|
konsola.title(`${commands.USER}`, colorPalette.USER);
|
|
2309
|
-
const verbose = program$
|
|
2339
|
+
const verbose = program$c.opts().verbose;
|
|
2310
2340
|
const { state } = session();
|
|
2311
2341
|
if (!requireAuthentication(state)) {
|
|
2312
2342
|
return;
|
|
@@ -2334,10 +2364,10 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
|
|
|
2334
2364
|
konsola.br();
|
|
2335
2365
|
});
|
|
2336
2366
|
|
|
2337
|
-
const program$
|
|
2338
|
-
const componentsCommand = program$
|
|
2367
|
+
const program$b = getProgram();
|
|
2368
|
+
const componentsCommand = program$b.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`);
|
|
2339
2369
|
|
|
2340
|
-
function isComponent(item) {
|
|
2370
|
+
function isComponent$1(item) {
|
|
2341
2371
|
return "schema" in item;
|
|
2342
2372
|
}
|
|
2343
2373
|
function isPreset(item) {
|
|
@@ -2371,7 +2401,7 @@ async function loadComponents(directoryPath, options) {
|
|
|
2371
2401
|
throw new Error('Internal tag is missing "id"!');
|
|
2372
2402
|
}
|
|
2373
2403
|
tagMap.set(item.id, item);
|
|
2374
|
-
} else if (isComponent(item)) {
|
|
2404
|
+
} else if (isComponent$1(item)) {
|
|
2375
2405
|
const existing = componentMap.get(item.name);
|
|
2376
2406
|
if (existing) {
|
|
2377
2407
|
duplicates.push(`Component "${item.name}" found in both "${existing.file}" and "${file}"`);
|
|
@@ -2407,44 +2437,20 @@ To fix this, either:
|
|
|
2407
2437
|
}
|
|
2408
2438
|
|
|
2409
2439
|
const DEFAULT_COMPONENTS_FILENAME = "components";
|
|
2410
|
-
const DEFAULT_GROUPS_FILENAME = "groups";
|
|
2440
|
+
const DEFAULT_GROUPS_FILENAME$1 = "groups";
|
|
2411
2441
|
const DEFAULT_PRESETS_FILENAME = "presets";
|
|
2412
2442
|
const DEFAULT_TAGS_FILENAME = "tags";
|
|
2413
2443
|
|
|
2414
|
-
async function fetchAllPages(fetchFunction, extractDataFunction) {
|
|
2415
|
-
const items = [];
|
|
2416
|
-
let page = 1;
|
|
2417
|
-
while (true) {
|
|
2418
|
-
const { data, response } = await fetchFunction(page);
|
|
2419
|
-
const totalHeader = response.headers.get("total");
|
|
2420
|
-
const fetchedItems = extractDataFunction(data);
|
|
2421
|
-
items.push(...fetchedItems);
|
|
2422
|
-
if (!totalHeader) {
|
|
2423
|
-
return items;
|
|
2424
|
-
}
|
|
2425
|
-
const total = Number(totalHeader);
|
|
2426
|
-
if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
|
|
2427
|
-
return items;
|
|
2428
|
-
}
|
|
2429
|
-
page++;
|
|
2430
|
-
}
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
2444
|
const fetchComponents = async (spaceId) => {
|
|
2434
2445
|
try {
|
|
2435
2446
|
const client = getMapiClient();
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
},
|
|
2444
|
-
throwOnError: true
|
|
2445
|
-
}),
|
|
2446
|
-
(data) => data?.components ?? []
|
|
2447
|
-
);
|
|
2447
|
+
const { data } = await client.components.list({
|
|
2448
|
+
path: {
|
|
2449
|
+
space_id: Number(spaceId)
|
|
2450
|
+
},
|
|
2451
|
+
throwOnError: true
|
|
2452
|
+
});
|
|
2453
|
+
return data?.components ?? [];
|
|
2448
2454
|
} catch (error) {
|
|
2449
2455
|
handleAPIError("pull_components", error);
|
|
2450
2456
|
}
|
|
@@ -2452,19 +2458,16 @@ const fetchComponents = async (spaceId) => {
|
|
|
2452
2458
|
const fetchComponent = async (spaceId, componentName) => {
|
|
2453
2459
|
try {
|
|
2454
2460
|
const client = getMapiClient();
|
|
2455
|
-
const
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
}),
|
|
2466
|
-
(data) => data?.components ?? []
|
|
2467
|
-
);
|
|
2461
|
+
const { data } = await client.components.list({
|
|
2462
|
+
path: {
|
|
2463
|
+
space_id: Number(spaceId)
|
|
2464
|
+
},
|
|
2465
|
+
query: {
|
|
2466
|
+
search: componentName
|
|
2467
|
+
},
|
|
2468
|
+
throwOnError: true
|
|
2469
|
+
});
|
|
2470
|
+
const matches = data?.components ?? [];
|
|
2468
2471
|
return matches.find((c) => c.name === componentName);
|
|
2469
2472
|
} catch (error) {
|
|
2470
2473
|
handleAPIError("pull_components", error, `Failed to fetch component ${componentName}`);
|
|
@@ -2531,7 +2534,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2531
2534
|
const presetsFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.${suffix}.json` : `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.json`);
|
|
2532
2535
|
await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2));
|
|
2533
2536
|
}
|
|
2534
|
-
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
|
|
2537
|
+
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
|
|
2535
2538
|
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
|
|
2536
2539
|
const internalTagsFilePath = join(resolvedPath, suffix ? `${DEFAULT_TAGS_FILENAME}.${suffix}.json` : `${DEFAULT_TAGS_FILENAME}.json`);
|
|
2537
2540
|
await saveToFile(internalTagsFilePath, JSON.stringify(internalTags, null, 2));
|
|
@@ -2541,7 +2544,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2541
2544
|
const componentsFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
|
|
2542
2545
|
await saveToFile(componentsFilePath, JSON.stringify(components, null, 2));
|
|
2543
2546
|
if (groups.length > 0) {
|
|
2544
|
-
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
|
|
2547
|
+
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
|
|
2545
2548
|
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
|
|
2546
2549
|
}
|
|
2547
2550
|
if (presets.length > 0) {
|
|
@@ -2557,6 +2560,27 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2557
2560
|
}
|
|
2558
2561
|
};
|
|
2559
2562
|
|
|
2563
|
+
function isSchemaField$1(value) {
|
|
2564
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
2565
|
+
}
|
|
2566
|
+
function toWritableSchema(schema) {
|
|
2567
|
+
if (!schema) {
|
|
2568
|
+
return void 0;
|
|
2569
|
+
}
|
|
2570
|
+
const result = {};
|
|
2571
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2572
|
+
if (isSchemaField$1(value)) {
|
|
2573
|
+
result[key] = value;
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
return result;
|
|
2577
|
+
}
|
|
2578
|
+
function toRequestTagIds(tagIds) {
|
|
2579
|
+
if (!tagIds) {
|
|
2580
|
+
return void 0;
|
|
2581
|
+
}
|
|
2582
|
+
return tagIds.map((id) => Number(id));
|
|
2583
|
+
}
|
|
2560
2584
|
const pushComponent = async (space, component) => {
|
|
2561
2585
|
try {
|
|
2562
2586
|
const client = getMapiClient();
|
|
@@ -2591,10 +2615,23 @@ const updateComponent = async (space, componentId, component) => {
|
|
|
2591
2615
|
}
|
|
2592
2616
|
};
|
|
2593
2617
|
const upsertComponent = async (space, component, existingId) => {
|
|
2618
|
+
const { name, display_name, schema, is_root, is_nestable, component_group_uuid, color, icon, preview_field, internal_tag_ids } = component;
|
|
2619
|
+
const payload = {
|
|
2620
|
+
name,
|
|
2621
|
+
display_name: display_name ?? void 0,
|
|
2622
|
+
schema: toWritableSchema(schema),
|
|
2623
|
+
is_root,
|
|
2624
|
+
is_nestable,
|
|
2625
|
+
component_group_uuid: component_group_uuid ?? void 0,
|
|
2626
|
+
color: color ?? void 0,
|
|
2627
|
+
icon: icon ?? void 0,
|
|
2628
|
+
preview_field: preview_field ?? void 0,
|
|
2629
|
+
internal_tag_ids: toRequestTagIds(internal_tag_ids)
|
|
2630
|
+
};
|
|
2594
2631
|
if (existingId) {
|
|
2595
|
-
return await updateComponent(space, existingId,
|
|
2632
|
+
return await updateComponent(space, existingId, payload);
|
|
2596
2633
|
} else {
|
|
2597
|
-
return await pushComponent(space,
|
|
2634
|
+
return await pushComponent(space, payload);
|
|
2598
2635
|
}
|
|
2599
2636
|
};
|
|
2600
2637
|
const pushComponentGroup = async (space, componentGroup) => {
|
|
@@ -2646,7 +2683,14 @@ const pushComponentPreset = async (space, preset) => {
|
|
|
2646
2683
|
space_id: Number(space)
|
|
2647
2684
|
},
|
|
2648
2685
|
body: {
|
|
2649
|
-
preset
|
|
2686
|
+
preset: {
|
|
2687
|
+
...preset,
|
|
2688
|
+
preset: preset.preset ?? void 0,
|
|
2689
|
+
image: preset.image ?? void 0,
|
|
2690
|
+
color: preset.color ?? void 0,
|
|
2691
|
+
icon: preset.icon ?? void 0,
|
|
2692
|
+
description: preset.description ?? void 0
|
|
2693
|
+
}
|
|
2650
2694
|
},
|
|
2651
2695
|
throwOnError: true
|
|
2652
2696
|
});
|
|
@@ -2663,7 +2707,14 @@ const updateComponentPreset = async (space, presetId, preset) => {
|
|
|
2663
2707
|
space_id: Number(space)
|
|
2664
2708
|
},
|
|
2665
2709
|
body: {
|
|
2666
|
-
preset
|
|
2710
|
+
preset: {
|
|
2711
|
+
...preset,
|
|
2712
|
+
preset: preset.preset ?? void 0,
|
|
2713
|
+
image: preset.image ?? void 0,
|
|
2714
|
+
color: preset.color ?? void 0,
|
|
2715
|
+
icon: preset.icon ?? void 0,
|
|
2716
|
+
description: preset.description ?? void 0
|
|
2717
|
+
}
|
|
2667
2718
|
},
|
|
2668
2719
|
throwOnError: true
|
|
2669
2720
|
});
|
|
@@ -2697,7 +2748,7 @@ const pushComponentInternalTag = async (space, componentInternalTag) => {
|
|
|
2697
2748
|
path: {
|
|
2698
2749
|
space_id: Number(space)
|
|
2699
2750
|
},
|
|
2700
|
-
body: componentInternalTag,
|
|
2751
|
+
body: { internal_tag: componentInternalTag },
|
|
2701
2752
|
throwOnError: true
|
|
2702
2753
|
});
|
|
2703
2754
|
return data.internal_tag;
|
|
@@ -2712,7 +2763,7 @@ const updateComponentInternalTag = async (space, tagId, componentInternalTag) =>
|
|
|
2712
2763
|
path: {
|
|
2713
2764
|
space_id: Number(space)
|
|
2714
2765
|
},
|
|
2715
|
-
body: componentInternalTag,
|
|
2766
|
+
body: { internal_tag: componentInternalTag },
|
|
2716
2767
|
throwOnError: true
|
|
2717
2768
|
});
|
|
2718
2769
|
return data.internal_tag;
|
|
@@ -3389,7 +3440,7 @@ function filterSpaceData(spaceData, selectors) {
|
|
|
3389
3440
|
if (filter && !minimatch(component.name, filter)) {
|
|
3390
3441
|
return false;
|
|
3391
3442
|
}
|
|
3392
|
-
if (groupUuids && !(component.component_group_uuid
|
|
3443
|
+
if (groupUuids && !(component.component_group_uuid != null && groupUuids.has(component.component_group_uuid))) {
|
|
3393
3444
|
return false;
|
|
3394
3445
|
}
|
|
3395
3446
|
if (tagIds) {
|
|
@@ -3453,7 +3504,7 @@ function resolveGroupSelector(groups, selector) {
|
|
|
3453
3504
|
while (p && !visited.has(p)) {
|
|
3454
3505
|
visited.add(p);
|
|
3455
3506
|
parts.unshift(nameOf(p) ?? p);
|
|
3456
|
-
p = byUuid.get(p)?.parent_uuid;
|
|
3507
|
+
p = byUuid.get(p)?.parent_uuid ?? null;
|
|
3457
3508
|
}
|
|
3458
3509
|
return parts.join("/");
|
|
3459
3510
|
});
|
|
@@ -4057,16 +4108,14 @@ const fetchSpace = async (spaceId) => {
|
|
|
4057
4108
|
handleAPIError("pull_spaces", error, `Failed to fetch space ${spaceId}`);
|
|
4058
4109
|
}
|
|
4059
4110
|
};
|
|
4060
|
-
const createSpace = async (space) => {
|
|
4111
|
+
const createSpace = async (space, query) => {
|
|
4061
4112
|
try {
|
|
4062
|
-
const { in_org, assign_partner, ...spaceData } = space;
|
|
4063
4113
|
const client = getMapiClient();
|
|
4064
4114
|
const { data } = await client.spaces.create({
|
|
4065
4115
|
body: {
|
|
4066
|
-
space
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
}
|
|
4116
|
+
space
|
|
4117
|
+
},
|
|
4118
|
+
query
|
|
4070
4119
|
});
|
|
4071
4120
|
return data?.space;
|
|
4072
4121
|
} catch (error) {
|
|
@@ -4077,10 +4126,10 @@ const createSpace = async (space) => {
|
|
|
4077
4126
|
const fetchLanguages = async (spaceId) => {
|
|
4078
4127
|
try {
|
|
4079
4128
|
const space = await fetchSpace(spaceId);
|
|
4080
|
-
if (space?.default_lang_name
|
|
4129
|
+
if (space?.default_lang_name && space?.languages?.length) {
|
|
4081
4130
|
return {
|
|
4082
|
-
default_lang_name: space
|
|
4083
|
-
languages: space
|
|
4131
|
+
default_lang_name: space.default_lang_name,
|
|
4132
|
+
languages: space.languages
|
|
4084
4133
|
};
|
|
4085
4134
|
}
|
|
4086
4135
|
} catch (error) {
|
|
@@ -4100,8 +4149,8 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
|
|
|
4100
4149
|
}
|
|
4101
4150
|
};
|
|
4102
4151
|
|
|
4103
|
-
const program$
|
|
4104
|
-
const languagesCommand = program$
|
|
4152
|
+
const program$a = getProgram();
|
|
4153
|
+
const languagesCommand = program$a.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`);
|
|
4105
4154
|
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");
|
|
4106
4155
|
pullCmd$3.action(async (options, command) => {
|
|
4107
4156
|
konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
|
|
@@ -4147,8 +4196,8 @@ pullCmd$3.action(async (options, command) => {
|
|
|
4147
4196
|
konsola.br();
|
|
4148
4197
|
});
|
|
4149
4198
|
|
|
4150
|
-
const program$
|
|
4151
|
-
const migrationsCommand = program$
|
|
4199
|
+
const program$9 = getProgram();
|
|
4200
|
+
const migrationsCommand = program$9.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`);
|
|
4152
4201
|
|
|
4153
4202
|
const getMigrationTemplate = () => {
|
|
4154
4203
|
return `export default function (block) {
|
|
@@ -4252,7 +4301,7 @@ const fetchStories = async (spaceId, params) => {
|
|
|
4252
4301
|
const fetchStory = async (spaceId, storyId) => {
|
|
4253
4302
|
try {
|
|
4254
4303
|
const client = getMapiClient();
|
|
4255
|
-
const { data } = await client.stories.get(storyId, {
|
|
4304
|
+
const { data } = await client.stories.get(Number(storyId), {
|
|
4256
4305
|
path: {
|
|
4257
4306
|
space_id: Number(spaceId)
|
|
4258
4307
|
},
|
|
@@ -4297,9 +4346,11 @@ const updateStory = async (spaceId, storyId, payload) => {
|
|
|
4297
4346
|
...payload.story,
|
|
4298
4347
|
// StoryUpdate2 expects `parent_id?: number`; normalize null → undefined.
|
|
4299
4348
|
parent_id: payload.story.parent_id ?? void 0
|
|
4300
|
-
}
|
|
4301
|
-
|
|
4302
|
-
|
|
4349
|
+
}
|
|
4350
|
+
},
|
|
4351
|
+
query: {
|
|
4352
|
+
force_update: payload.force_update === "1",
|
|
4353
|
+
...payload.publish ? { publish: Boolean(payload.publish) } : {}
|
|
4303
4354
|
},
|
|
4304
4355
|
throwOnError: true
|
|
4305
4356
|
});
|
|
@@ -4396,6 +4447,35 @@ const prefetchTargetStoriesByKeys = async (spaceId, keys, options) => {
|
|
|
4396
4447
|
return result;
|
|
4397
4448
|
};
|
|
4398
4449
|
|
|
4450
|
+
function parseFilterQuery(input) {
|
|
4451
|
+
const trimmed = input.trim();
|
|
4452
|
+
if (!trimmed) {
|
|
4453
|
+
return {};
|
|
4454
|
+
}
|
|
4455
|
+
if (trimmed.startsWith("{")) {
|
|
4456
|
+
return JSON.parse(trimmed);
|
|
4457
|
+
}
|
|
4458
|
+
const result = {};
|
|
4459
|
+
for (const clause of trimmed.split("&")) {
|
|
4460
|
+
if (!clause) {
|
|
4461
|
+
continue;
|
|
4462
|
+
}
|
|
4463
|
+
const eq = clause.indexOf("=");
|
|
4464
|
+
if (eq === -1) {
|
|
4465
|
+
continue;
|
|
4466
|
+
}
|
|
4467
|
+
const path = clause.slice(0, eq);
|
|
4468
|
+
const value = clause.slice(eq + 1);
|
|
4469
|
+
const keys = [...path.matchAll(/\[([^\]]+)\]/g)].map((match) => match[1]);
|
|
4470
|
+
if (keys.length < 2) {
|
|
4471
|
+
continue;
|
|
4472
|
+
}
|
|
4473
|
+
const [field, operation] = keys;
|
|
4474
|
+
result[field] = { ...result[field], [operation]: value };
|
|
4475
|
+
}
|
|
4476
|
+
return result;
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4399
4479
|
const PIPELINE_BACKPRESSURE_MULTIPLIER = 2;
|
|
4400
4480
|
const DEFAULT_PIPELINE_BACKPRESSURE = 12;
|
|
4401
4481
|
function createPipelineBackpressureLock(limit) {
|
|
@@ -4419,16 +4499,13 @@ const ERROR_CODES = {
|
|
|
4419
4499
|
async function* storiesIterator(spaceId, params, onTotal) {
|
|
4420
4500
|
try {
|
|
4421
4501
|
let perPage = 500;
|
|
4422
|
-
const
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
transformedParams.contain_component = params.componentName;
|
|
4427
|
-
delete transformedParams.componentName;
|
|
4502
|
+
const { componentName, query, ...rest } = params ?? {};
|
|
4503
|
+
const transformedParams = { ...rest };
|
|
4504
|
+
if (componentName) {
|
|
4505
|
+
transformedParams.contain_component = componentName;
|
|
4428
4506
|
}
|
|
4429
|
-
if (
|
|
4430
|
-
transformedParams.filter_query =
|
|
4431
|
-
delete transformedParams.query;
|
|
4507
|
+
if (query) {
|
|
4508
|
+
transformedParams.filter_query = parseFilterQuery(query);
|
|
4432
4509
|
}
|
|
4433
4510
|
const result = await fetchStories(spaceId, {
|
|
4434
4511
|
...transformedParams,
|
|
@@ -4753,8 +4830,8 @@ class MigrationStream extends Transform {
|
|
|
4753
4830
|
id: story.id,
|
|
4754
4831
|
name: story.name || "",
|
|
4755
4832
|
content: story.content,
|
|
4756
|
-
published: story.published,
|
|
4757
|
-
unpublished_changes: story.unpublished_changes
|
|
4833
|
+
published: story.published ?? void 0,
|
|
4834
|
+
unpublished_changes: story.unpublished_changes ?? void 0
|
|
4758
4835
|
},
|
|
4759
4836
|
migrationTimestamp: this.timestamp,
|
|
4760
4837
|
migrationNames
|
|
@@ -4770,8 +4847,8 @@ class MigrationStream extends Transform {
|
|
|
4770
4847
|
storyId: story.id,
|
|
4771
4848
|
name: story.name,
|
|
4772
4849
|
content: storyContent,
|
|
4773
|
-
published: story.published,
|
|
4774
|
-
unpublished_changes: story.unpublished_changes
|
|
4850
|
+
published: story.published ?? void 0,
|
|
4851
|
+
unpublished_changes: story.unpublished_changes ?? void 0
|
|
4775
4852
|
};
|
|
4776
4853
|
} else if (processed && !contentChanged) {
|
|
4777
4854
|
this.results.skipped.push({
|
|
@@ -4913,7 +4990,6 @@ class UpdateStream extends Writable {
|
|
|
4913
4990
|
const payload = {
|
|
4914
4991
|
story: {
|
|
4915
4992
|
content,
|
|
4916
|
-
id: storyId,
|
|
4917
4993
|
name: storyName
|
|
4918
4994
|
},
|
|
4919
4995
|
force_update: "1"
|
|
@@ -5162,7 +5238,6 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
|
|
|
5162
5238
|
const payload = {
|
|
5163
5239
|
story: {
|
|
5164
5240
|
content: story.content,
|
|
5165
|
-
id: story.storyId,
|
|
5166
5241
|
name: story.name
|
|
5167
5242
|
},
|
|
5168
5243
|
force_update: "1"
|
|
@@ -5202,8 +5277,8 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
|
|
|
5202
5277
|
}
|
|
5203
5278
|
});
|
|
5204
5279
|
|
|
5205
|
-
const program$
|
|
5206
|
-
const typesCommand = program$
|
|
5280
|
+
const program$8 = getProgram();
|
|
5281
|
+
const typesCommand = program$8.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`);
|
|
5207
5282
|
|
|
5208
5283
|
const getAssetJSONSchema = (title) => ({
|
|
5209
5284
|
$id: "#/asset",
|
|
@@ -6205,7 +6280,7 @@ const deleteDatasourceEntry = async (spaceId, entryId) => {
|
|
|
6205
6280
|
handleAPIError("delete_datasource_entry", error, `Failed to delete datasource entry ${entryId}`);
|
|
6206
6281
|
}
|
|
6207
6282
|
};
|
|
6208
|
-
function isDatasource(item) {
|
|
6283
|
+
function isDatasource$1(item) {
|
|
6209
6284
|
return typeof item === "object" && item !== null && "slug" in item && typeof item.slug === "string";
|
|
6210
6285
|
}
|
|
6211
6286
|
const readDatasourcesFiles = async (options) => {
|
|
@@ -6238,7 +6313,7 @@ const readDatasourcesFiles = async (options) => {
|
|
|
6238
6313
|
continue;
|
|
6239
6314
|
}
|
|
6240
6315
|
for (const item of data) {
|
|
6241
|
-
if (isDatasource(item)) {
|
|
6316
|
+
if (isDatasource$1(item)) {
|
|
6242
6317
|
const existing = datasourceMap.get(item.slug);
|
|
6243
6318
|
if (existing) {
|
|
6244
6319
|
duplicates.push(`Datasource "${item.slug}" found in both "${existing.file}" and "${file}"`);
|
|
@@ -6338,8 +6413,8 @@ generateCmd.action(async (options, command) => {
|
|
|
6338
6413
|
}
|
|
6339
6414
|
});
|
|
6340
6415
|
|
|
6341
|
-
const program$
|
|
6342
|
-
const datasourcesCommand = program$
|
|
6416
|
+
const program$7 = getProgram();
|
|
6417
|
+
const datasourcesCommand = program$7.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
|
|
6343
6418
|
|
|
6344
6419
|
const DEFAULT_DATASOURCES_FILENAME = "datasources";
|
|
6345
6420
|
|
|
@@ -7005,13 +7080,13 @@ async function promptForLogin(verbose) {
|
|
|
7005
7080
|
return null;
|
|
7006
7081
|
}
|
|
7007
7082
|
}
|
|
7008
|
-
const program$
|
|
7009
|
-
program$
|
|
7083
|
+
const program$6 = getProgram();
|
|
7084
|
+
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(
|
|
7010
7085
|
"-r, --region <region>",
|
|
7011
7086
|
`The region to apply to the generated project template (does not affect space creation).`
|
|
7012
7087
|
).action(async (projectPath, options) => {
|
|
7013
7088
|
ui.title(`${commands.CREATE}`, colorPalette.CREATE);
|
|
7014
|
-
const verbose = program$
|
|
7089
|
+
const verbose = program$6.opts().verbose;
|
|
7015
7090
|
const { template, blueprint, token } = options;
|
|
7016
7091
|
if (options.region && !isRegion(options.region)) {
|
|
7017
7092
|
handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
|
|
@@ -7184,13 +7259,13 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
7184
7259
|
name: toHumanReadable(projectName),
|
|
7185
7260
|
domain: blueprintDomain
|
|
7186
7261
|
};
|
|
7262
|
+
const createSpaceQuery = {};
|
|
7187
7263
|
if (whereToCreateSpace === "org") {
|
|
7188
|
-
|
|
7189
|
-
spaceToCreate.in_org = true;
|
|
7264
|
+
createSpaceQuery.in_org = true;
|
|
7190
7265
|
} else if (whereToCreateSpace === "partner") {
|
|
7191
|
-
|
|
7266
|
+
createSpaceQuery.assign_partner = true;
|
|
7192
7267
|
}
|
|
7193
|
-
createdSpace = await createSpace(spaceToCreate);
|
|
7268
|
+
createdSpace = await createSpace(spaceToCreate, createSpaceQuery);
|
|
7194
7269
|
spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`);
|
|
7195
7270
|
if (createdSpace?.first_token) {
|
|
7196
7271
|
await handleEnvFileCreation(resolvedPath, createdSpace.first_token, region, technologyTemplate);
|
|
@@ -7230,8 +7305,8 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
7230
7305
|
ui.br();
|
|
7231
7306
|
});
|
|
7232
7307
|
|
|
7233
|
-
const program$
|
|
7234
|
-
const logsCommand = program$
|
|
7308
|
+
const program$5 = getProgram();
|
|
7309
|
+
const logsCommand = program$5.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`);
|
|
7235
7310
|
|
|
7236
7311
|
const listCmd$1 = logsCommand.command("list").description("List logs").option("-s, --space <space>", "space ID");
|
|
7237
7312
|
listCmd$1.action(async (_options, command) => {
|
|
@@ -7256,8 +7331,8 @@ pruneCmd$1.action(async (options, command) => {
|
|
|
7256
7331
|
ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
7257
7332
|
});
|
|
7258
7333
|
|
|
7259
|
-
const program$
|
|
7260
|
-
const reportsCommand = program$
|
|
7334
|
+
const program$4 = getProgram();
|
|
7335
|
+
const reportsCommand = program$4.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.");
|
|
7261
7336
|
|
|
7262
7337
|
const listCmd = reportsCommand.command("list").description("List reports").option("-s, --space <space>", "space ID");
|
|
7263
7338
|
listCmd.action(async (_options, command) => {
|
|
@@ -7282,8 +7357,8 @@ pruneCmd.action(async (options, command) => {
|
|
|
7282
7357
|
ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
7283
7358
|
});
|
|
7284
7359
|
|
|
7285
|
-
const program$
|
|
7286
|
-
const assetsCommand = program$
|
|
7360
|
+
const program$3 = getProgram();
|
|
7361
|
+
const assetsCommand = program$3.command(commands.ASSETS).description(`Manage your space's assets`);
|
|
7287
7362
|
|
|
7288
7363
|
const fetchAssets = async ({ spaceId, params }) => {
|
|
7289
7364
|
try {
|
|
@@ -7331,7 +7406,7 @@ const createAssetInternalTag = async (spaceId, name) => {
|
|
|
7331
7406
|
const client = getMapiClient();
|
|
7332
7407
|
const { data } = await client.internalTags.create({
|
|
7333
7408
|
path: { space_id: Number(spaceId) },
|
|
7334
|
-
body: { name, object_type: "asset" },
|
|
7409
|
+
body: { internal_tag: { name, object_type: "asset" } },
|
|
7335
7410
|
throwOnError: true
|
|
7336
7411
|
});
|
|
7337
7412
|
const tag = data?.internal_tag;
|
|
@@ -7455,7 +7530,7 @@ const updateAsset = async (id, asset, { spaceId, fileBuffer }) => {
|
|
|
7455
7530
|
const createAsset = async (asset, fileBuffer, { spaceId }) => {
|
|
7456
7531
|
try {
|
|
7457
7532
|
const client = getMapiClient();
|
|
7458
|
-
const { id: _id, ...assetBody } = asset;
|
|
7533
|
+
const { id: _id, filename: _filename, ...assetBody } = asset;
|
|
7459
7534
|
return await client.assets.create({
|
|
7460
7535
|
body: assetBody,
|
|
7461
7536
|
file: fileBuffer,
|
|
@@ -7675,6 +7750,26 @@ const parseAssetData = (raw) => {
|
|
|
7675
7750
|
throw new Error(`Invalid --data JSON: ${toError(maybeError).message}`);
|
|
7676
7751
|
}
|
|
7677
7752
|
};
|
|
7753
|
+
const toAssetUpload = (partial, shortFilename) => {
|
|
7754
|
+
const nullToUndef = (value) => value ?? void 0;
|
|
7755
|
+
return {
|
|
7756
|
+
id: partial.id,
|
|
7757
|
+
filename: partial.filename,
|
|
7758
|
+
short_filename: shortFilename,
|
|
7759
|
+
asset_folder_id: nullToUndef(partial.asset_folder_id),
|
|
7760
|
+
ext_id: nullToUndef(partial.ext_id),
|
|
7761
|
+
alt: nullToUndef(partial.alt),
|
|
7762
|
+
copyright: nullToUndef(partial.copyright),
|
|
7763
|
+
title: nullToUndef(partial.title),
|
|
7764
|
+
source: nullToUndef(partial.source),
|
|
7765
|
+
expire_at: nullToUndef(partial.expire_at),
|
|
7766
|
+
publish_at: nullToUndef(partial.publish_at),
|
|
7767
|
+
focus: nullToUndef(partial.focus),
|
|
7768
|
+
is_private: nullToUndef(partial.is_private),
|
|
7769
|
+
internal_tag_ids: partial.internal_tag_ids,
|
|
7770
|
+
meta_data: nullToUndef(partial.meta_data)
|
|
7771
|
+
};
|
|
7772
|
+
};
|
|
7678
7773
|
const getSidecarFilename = (assetBinaryPath) => {
|
|
7679
7774
|
return join(dirname(assetBinaryPath), `${basename(assetBinaryPath, extname(assetBinaryPath))}.json`);
|
|
7680
7775
|
};
|
|
@@ -7703,7 +7798,7 @@ const isRemoteSource = (assetBinaryPath) => {
|
|
|
7703
7798
|
return false;
|
|
7704
7799
|
}
|
|
7705
7800
|
};
|
|
7706
|
-
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.
|
|
7801
|
+
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.new_filename);
|
|
7707
7802
|
const loadAssetMap = async (manifestFile) => {
|
|
7708
7803
|
const manifest = await loadManifest(manifestFile);
|
|
7709
7804
|
const entries = manifest.filter(isValidManifestEntry).map((e) => [
|
|
@@ -8132,8 +8227,10 @@ const readLocalAssetsStream = ({
|
|
|
8132
8227
|
const sidecar = await loadSidecarAssetData(binaryFilePath);
|
|
8133
8228
|
const shortFilename = sidecar.short_filename || (sidecar.filename ? basename(sidecar.filename) : void 0) || file;
|
|
8134
8229
|
const asset = {
|
|
8135
|
-
...sidecar,
|
|
8136
|
-
|
|
8230
|
+
...toAssetUpload(sidecar, shortFilename),
|
|
8231
|
+
// Carry the read-only tag detail for source→target tag-name
|
|
8232
|
+
// translation in `processAsset`; it is stripped before the API call.
|
|
8233
|
+
...sidecar.internal_tags_list ? { internal_tags_list: sidecar.internal_tags_list } : {}
|
|
8137
8234
|
};
|
|
8138
8235
|
const fileBuffer = await readFile$1(binaryFilePath);
|
|
8139
8236
|
const sidecarPath = getSidecarFilename(binaryFilePath);
|
|
@@ -8217,7 +8314,7 @@ const mapInternalTagIds = (sourceIds, sourceTags, assetInternalTagsByName, onUnm
|
|
|
8217
8314
|
const sourceName = sourceNamesById.get(sourceId);
|
|
8218
8315
|
const targetId = typeof sourceName === "string" ? assetInternalTagsByName.get(sourceName) : void 0;
|
|
8219
8316
|
if (typeof targetId === "number") {
|
|
8220
|
-
mapped.push(
|
|
8317
|
+
mapped.push(targetId);
|
|
8221
8318
|
} else {
|
|
8222
8319
|
onUnmappedTag?.({ sourceId, name: sourceName });
|
|
8223
8320
|
}
|
|
@@ -8271,12 +8368,13 @@ const processAsset = async ({
|
|
|
8271
8368
|
if (maps.resolveSharedTagIds) {
|
|
8272
8369
|
return maps.resolveSharedTagIds(sourceIds, sourceTags);
|
|
8273
8370
|
}
|
|
8274
|
-
return maps.assetInternalTagsByName ? mapInternalTagIds(sourceIds, sourceTags, maps.assetInternalTagsByName, onUnmappedTag) : (sourceIds ?? []).map((id) =>
|
|
8371
|
+
return maps.assetInternalTagsByName ? mapInternalTagIds(sourceIds, sourceTags, maps.assetInternalTagsByName, onUnmappedTag) : (sourceIds ?? []).map((id) => Number(id));
|
|
8275
8372
|
};
|
|
8276
8373
|
let newRemoteAsset;
|
|
8277
8374
|
let status;
|
|
8278
8375
|
if (remoteAsset) {
|
|
8279
|
-
const updateTagIds =
|
|
8376
|
+
const updateTagIds = localAsset.internal_tag_ids ? await resolveInternalTagIds(localAsset.internal_tag_ids) : remoteAsset.internal_tag_ids;
|
|
8377
|
+
const nullToUndef = (v) => v ?? void 0;
|
|
8280
8378
|
const updatePayload = {
|
|
8281
8379
|
asset_folder_id: remoteFolderId,
|
|
8282
8380
|
alt: "alt" in localAsset ? localAsset.alt : remoteAsset.alt,
|
|
@@ -8285,25 +8383,29 @@ const processAsset = async ({
|
|
|
8285
8383
|
source: "source" in localAsset ? localAsset.source : remoteAsset.source,
|
|
8286
8384
|
is_private: "is_private" in localAsset ? localAsset.is_private : remoteAsset.is_private,
|
|
8287
8385
|
focus: "focus" in localAsset ? localAsset.focus : remoteAsset.focus,
|
|
8288
|
-
expire_at: "expire_at" in localAsset ? localAsset.expire_at : remoteAsset.expire_at,
|
|
8289
|
-
publish_at: "publish_at" in localAsset ? localAsset.publish_at : remoteAsset.publish_at,
|
|
8386
|
+
expire_at: nullToUndef("expire_at" in localAsset ? localAsset.expire_at : remoteAsset.expire_at),
|
|
8387
|
+
publish_at: nullToUndef("publish_at" in localAsset ? localAsset.publish_at : remoteAsset.publish_at),
|
|
8290
8388
|
internal_tag_ids: updateTagIds,
|
|
8291
|
-
meta_data: "meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data
|
|
8389
|
+
meta_data: nullToUndef("meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data)
|
|
8292
8390
|
};
|
|
8293
8391
|
await transports.updateAsset(
|
|
8294
8392
|
remoteAsset.id,
|
|
8295
8393
|
{ ...updatePayload, short_filename: remoteAsset.short_filename },
|
|
8296
8394
|
fileBuffer
|
|
8297
8395
|
);
|
|
8298
|
-
newRemoteAsset = {
|
|
8396
|
+
newRemoteAsset = {
|
|
8397
|
+
...remoteAsset,
|
|
8398
|
+
...updatePayload,
|
|
8399
|
+
is_private: updatePayload.is_private ?? remoteAsset.is_private
|
|
8400
|
+
};
|
|
8299
8401
|
status = "updated";
|
|
8300
8402
|
} else if (hasProp(localAsset, "short_filename")) {
|
|
8301
|
-
const
|
|
8302
|
-
const
|
|
8303
|
-
const
|
|
8403
|
+
const mappedTagIds = localAsset.internal_tag_ids ? await resolveInternalTagIds(localAsset.internal_tag_ids) : void 0;
|
|
8404
|
+
const size = hasProp(localAsset, "size") ? localAsset.size : hasProp(localAsset, "filename") ? extractAssetSizeFromFilename(localAsset.filename) : void 0;
|
|
8405
|
+
const { internal_tag_ids: _sourceTagIds, ...uploadBase } = toAssetUpload(localAsset, localAsset.short_filename);
|
|
8304
8406
|
const createPayload = {
|
|
8305
|
-
...
|
|
8306
|
-
asset_folder_id: remoteFolderId,
|
|
8407
|
+
...uploadBase,
|
|
8408
|
+
asset_folder_id: remoteFolderId ?? void 0,
|
|
8307
8409
|
...mappedTagIds !== void 0 ? { internal_tag_ids: mappedTagIds } : {},
|
|
8308
8410
|
...size !== void 0 ? { size } : {}
|
|
8309
8411
|
};
|
|
@@ -8419,7 +8521,7 @@ const makeSharedTagResolver = ({ spaceId, libraryId }) => {
|
|
|
8419
8521
|
}
|
|
8420
8522
|
}
|
|
8421
8523
|
if (sharedId !== void 0) {
|
|
8422
|
-
remapped.push(
|
|
8524
|
+
remapped.push(sharedId);
|
|
8423
8525
|
}
|
|
8424
8526
|
}
|
|
8425
8527
|
return remapped;
|
|
@@ -8829,8 +8931,8 @@ const traverseAndMapBySchema = (data, {
|
|
|
8829
8931
|
const dataNew = { ...data };
|
|
8830
8932
|
for (const [fieldName, fieldValue] of Object.entries(data)) {
|
|
8831
8933
|
const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
|
|
8832
|
-
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema
|
|
8833
|
-
const fieldRefMapper = typeof fieldType === "string"
|
|
8934
|
+
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema ? fieldSchema.type : void 0;
|
|
8935
|
+
const fieldRefMapper = typeof fieldType === "string" ? fieldRefMappers2[fieldType] : void 0;
|
|
8834
8936
|
if (fieldRefMapper) {
|
|
8835
8937
|
dataNew[fieldName] = fieldRefMapper(fieldValue, {
|
|
8836
8938
|
schema: fieldSchema,
|
|
@@ -8895,6 +8997,9 @@ const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRef
|
|
|
8895
8997
|
fieldRefMappers: fieldRefMappers2
|
|
8896
8998
|
});
|
|
8897
8999
|
const multilinkFieldRefMapper = (data, { maps }) => {
|
|
9000
|
+
if (!data || typeof data !== "object") {
|
|
9001
|
+
return data;
|
|
9002
|
+
}
|
|
8898
9003
|
if (data.linktype !== "story") {
|
|
8899
9004
|
return data;
|
|
8900
9005
|
}
|
|
@@ -8914,6 +9019,9 @@ const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMap
|
|
|
8914
9019
|
}));
|
|
8915
9020
|
};
|
|
8916
9021
|
const assetFieldRefMapper = (data, { maps }) => {
|
|
9022
|
+
if (!data || typeof data !== "object") {
|
|
9023
|
+
return data;
|
|
9024
|
+
}
|
|
8917
9025
|
const mappedAsset = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
|
|
8918
9026
|
if (!mappedAsset) {
|
|
8919
9027
|
return data;
|
|
@@ -8931,7 +9039,7 @@ const multiassetFieldRefMapper = (data, options) => {
|
|
|
8931
9039
|
return data.map((d) => assetFieldRefMapper(d, options));
|
|
8932
9040
|
};
|
|
8933
9041
|
const optionsFieldRefMapper = (data, { schema, maps }) => {
|
|
8934
|
-
if (schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
9042
|
+
if (!schema || !("source" in schema) || schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
8935
9043
|
return data;
|
|
8936
9044
|
}
|
|
8937
9045
|
return data.map((d) => maps.stories?.get(d) || d);
|
|
@@ -9824,11 +9932,7 @@ pushCmd$1.action(async (assetInput, options, command) => {
|
|
|
9824
9932
|
const sourceBasename = isRemoteSource(assetBinaryPath) ? basename(new URL(assetBinaryPath).pathname) : basename(assetBinaryPath);
|
|
9825
9933
|
const shortFilename = options.shortFilename || assetDataPartial.short_filename || sourceBasename;
|
|
9826
9934
|
const folderId = options.folder ? Number(options.folder) : void 0;
|
|
9827
|
-
assetData = {
|
|
9828
|
-
...assetDataPartial,
|
|
9829
|
-
short_filename: shortFilename,
|
|
9830
|
-
asset_folder_id: folderId
|
|
9831
|
-
};
|
|
9935
|
+
assetData = { ...toAssetUpload(assetDataPartial, shortFilename), asset_folder_id: folderId };
|
|
9832
9936
|
}
|
|
9833
9937
|
try {
|
|
9834
9938
|
for (const scope of scopes) {
|
|
@@ -10036,8 +10140,8 @@ transferCmd.action(async (assetIds, options, command) => {
|
|
|
10036
10140
|
process.exitCode = summary.failed > 0 ? 1 : 0;
|
|
10037
10141
|
});
|
|
10038
10142
|
|
|
10039
|
-
const program$
|
|
10040
|
-
const storiesCommand = program$
|
|
10143
|
+
const program$2 = getProgram();
|
|
10144
|
+
const storiesCommand = program$2.command(commands.STORIES).description(`Manage your space's stories`);
|
|
10041
10145
|
|
|
10042
10146
|
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.`);
|
|
10043
10147
|
pullCmd.action(async (options, command) => {
|
|
@@ -10073,7 +10177,7 @@ pullCmd.action(async (options, command) => {
|
|
|
10073
10177
|
fetchStoriesStream({
|
|
10074
10178
|
spaceId: space,
|
|
10075
10179
|
params: {
|
|
10076
|
-
filter_query: options.query,
|
|
10180
|
+
filter_query: options.query ? parseFilterQuery(options.query) : void 0,
|
|
10077
10181
|
starts_with: options.startsWith
|
|
10078
10182
|
},
|
|
10079
10183
|
setTotalPages: (totalPages) => {
|
|
@@ -10542,6 +10646,1814 @@ pushCmd.action(async (options, command) => {
|
|
|
10542
10646
|
}
|
|
10543
10647
|
});
|
|
10544
10648
|
|
|
10649
|
+
const program$1 = getProgram();
|
|
10650
|
+
const schemaCommand = program$1.command(commands.SCHEMA).description(`Manage your space's schema from code`);
|
|
10651
|
+
|
|
10652
|
+
function isRecord(value) {
|
|
10653
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10654
|
+
}
|
|
10655
|
+
const COMPONENT_STRIP_KEYS = /* @__PURE__ */ new Set([
|
|
10656
|
+
"id",
|
|
10657
|
+
"created_at",
|
|
10658
|
+
"updated_at",
|
|
10659
|
+
"real_name",
|
|
10660
|
+
// API-computed display/technical name, read-only
|
|
10661
|
+
"preset_id",
|
|
10662
|
+
// Instance-level preset selection, not part of schema definition
|
|
10663
|
+
"all_presets",
|
|
10664
|
+
// Computed list of presets, managed via /presets API
|
|
10665
|
+
"internal_tags_list",
|
|
10666
|
+
// Read-only expanded form of internal_tag_ids ({id, name} objects)
|
|
10667
|
+
"content_type_asset_preview",
|
|
10668
|
+
// Read-only, not in ComponentCreate/ComponentUpdate
|
|
10669
|
+
"image",
|
|
10670
|
+
// Read-only preview image URL
|
|
10671
|
+
"preview_tmpl",
|
|
10672
|
+
// Read-only preview template
|
|
10673
|
+
"metadata",
|
|
10674
|
+
// Not in current API types, stripped defensively
|
|
10675
|
+
"component_group_uuid"
|
|
10676
|
+
// UI grouping; stripped by default — kept for diffing only when a block opts into the group escape hatch
|
|
10677
|
+
]);
|
|
10678
|
+
const DATASOURCE_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
|
|
10679
|
+
const DATASOURCE_DIMENSION_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "datasource_id", "created_at", "updated_at"]);
|
|
10680
|
+
const DATASOURCE_DEFAULTS = {
|
|
10681
|
+
dimensions: []
|
|
10682
|
+
};
|
|
10683
|
+
const COMPONENT_DEFAULTS = {
|
|
10684
|
+
display_name: "",
|
|
10685
|
+
description: "",
|
|
10686
|
+
color: "",
|
|
10687
|
+
icon: "",
|
|
10688
|
+
preview_field: "",
|
|
10689
|
+
internal_tag_ids: []
|
|
10690
|
+
};
|
|
10691
|
+
function applyDefaults(entity, defaults) {
|
|
10692
|
+
const result = { ...entity };
|
|
10693
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
10694
|
+
if (result[key] === void 0 || result[key] === null) {
|
|
10695
|
+
Object.assign(result, { [key]: defaultValue });
|
|
10696
|
+
}
|
|
10697
|
+
}
|
|
10698
|
+
return result;
|
|
10699
|
+
}
|
|
10700
|
+
const INDENT = " ";
|
|
10701
|
+
function quoteString(value) {
|
|
10702
|
+
const escaped = JSON.stringify(value).slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'");
|
|
10703
|
+
return `'${escaped}'`;
|
|
10704
|
+
}
|
|
10705
|
+
function formatValue(value, depth) {
|
|
10706
|
+
const indent = INDENT.repeat(depth);
|
|
10707
|
+
const innerIndent = INDENT.repeat(depth + 1);
|
|
10708
|
+
if (value === null || value === void 0) {
|
|
10709
|
+
return String(value);
|
|
10710
|
+
}
|
|
10711
|
+
if (typeof value === "string") {
|
|
10712
|
+
return quoteString(value);
|
|
10713
|
+
}
|
|
10714
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
10715
|
+
return String(value);
|
|
10716
|
+
}
|
|
10717
|
+
if (Array.isArray(value)) {
|
|
10718
|
+
if (value.length === 0) {
|
|
10719
|
+
return "[]";
|
|
10720
|
+
}
|
|
10721
|
+
const items = value.map((item) => `${innerIndent}${formatValue(item, depth + 1)},`);
|
|
10722
|
+
return `[
|
|
10723
|
+
${items.join("\n")}
|
|
10724
|
+
${indent}]`;
|
|
10725
|
+
}
|
|
10726
|
+
if (typeof value === "object") {
|
|
10727
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0 && v !== null).sort(([a], [b]) => a.localeCompare(b));
|
|
10728
|
+
if (entries.length === 0) {
|
|
10729
|
+
return "{}";
|
|
10730
|
+
}
|
|
10731
|
+
const props = entries.map(
|
|
10732
|
+
([key, val]) => `${innerIndent}${key}: ${formatValue(val, depth + 1)},`
|
|
10733
|
+
);
|
|
10734
|
+
return `{
|
|
10735
|
+
${props.join("\n")}
|
|
10736
|
+
${indent}}`;
|
|
10737
|
+
}
|
|
10738
|
+
return String(value);
|
|
10739
|
+
}
|
|
10740
|
+
function fileTimestamp(iso) {
|
|
10741
|
+
return iso.replace(/\D/g, "").slice(0, 14);
|
|
10742
|
+
}
|
|
10743
|
+
function displayPath(filePath, userPath) {
|
|
10744
|
+
return userPath && isAbsolute(userPath) ? filePath : relative(process.cwd(), filePath);
|
|
10745
|
+
}
|
|
10746
|
+
function stripKeys(obj, keysToStrip) {
|
|
10747
|
+
const result = {};
|
|
10748
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
10749
|
+
if (!keysToStrip.has(key) && value !== void 0 && value !== null) {
|
|
10750
|
+
result[key] = value;
|
|
10751
|
+
}
|
|
10752
|
+
}
|
|
10753
|
+
return result;
|
|
10754
|
+
}
|
|
10755
|
+
|
|
10756
|
+
function mapFieldToWire(field) {
|
|
10757
|
+
const { name, allow, datasource, ...rest } = field;
|
|
10758
|
+
const value = { ...rest };
|
|
10759
|
+
if (allow !== void 0) {
|
|
10760
|
+
value.component_whitelist = allow;
|
|
10761
|
+
if (rest.type === "bloks") {
|
|
10762
|
+
value.restrict_components = true;
|
|
10763
|
+
value.restrict_type = "";
|
|
10764
|
+
}
|
|
10765
|
+
}
|
|
10766
|
+
if (datasource !== void 0) {
|
|
10767
|
+
value.datasource_slug = datasource;
|
|
10768
|
+
}
|
|
10769
|
+
return { name: typeof name === "string" ? name : "", value };
|
|
10770
|
+
}
|
|
10771
|
+
function mapBlockToWire(block) {
|
|
10772
|
+
const { fields, ...rest } = block;
|
|
10773
|
+
const schema = {};
|
|
10774
|
+
if (Array.isArray(fields)) {
|
|
10775
|
+
for (const field of fields) {
|
|
10776
|
+
if (!isRecord(field)) {
|
|
10777
|
+
continue;
|
|
10778
|
+
}
|
|
10779
|
+
const { name, value } = mapFieldToWire(field);
|
|
10780
|
+
if (name) {
|
|
10781
|
+
schema[name] = value;
|
|
10782
|
+
}
|
|
10783
|
+
}
|
|
10784
|
+
}
|
|
10785
|
+
return { ...rest, schema };
|
|
10786
|
+
}
|
|
10787
|
+
function mapDatasourceToWire(datasource) {
|
|
10788
|
+
return datasource;
|
|
10789
|
+
}
|
|
10790
|
+
|
|
10791
|
+
function isComponent(value) {
|
|
10792
|
+
return isRecord(value) && typeof value.name === "string" && Array.isArray(value.fields);
|
|
10793
|
+
}
|
|
10794
|
+
function isDatasource(value) {
|
|
10795
|
+
return isRecord(value) && typeof value.name === "string" && typeof value.slug === "string" && !Array.isArray(value.fields);
|
|
10796
|
+
}
|
|
10797
|
+
function isSchemaObject(value) {
|
|
10798
|
+
return isRecord(value) && ("blocks" in value || "datasources" in value);
|
|
10799
|
+
}
|
|
10800
|
+
function emptySchemaData() {
|
|
10801
|
+
return { components: [], datasources: [] };
|
|
10802
|
+
}
|
|
10803
|
+
function classifyExports(moduleExports) {
|
|
10804
|
+
const data = emptySchemaData();
|
|
10805
|
+
const seenComponents = /* @__PURE__ */ new Set();
|
|
10806
|
+
const seenDatasources = /* @__PURE__ */ new Set();
|
|
10807
|
+
function collect(value) {
|
|
10808
|
+
if (isComponent(value)) {
|
|
10809
|
+
if (seenComponents.has(value.name)) {
|
|
10810
|
+
return;
|
|
10811
|
+
}
|
|
10812
|
+
seenComponents.add(value.name);
|
|
10813
|
+
data.components.push(mapBlockToWire(value));
|
|
10814
|
+
} else if (isDatasource(value)) {
|
|
10815
|
+
if (seenDatasources.has(value.name)) {
|
|
10816
|
+
return;
|
|
10817
|
+
}
|
|
10818
|
+
seenDatasources.add(value.name);
|
|
10819
|
+
data.datasources.push(mapDatasourceToWire(value));
|
|
10820
|
+
}
|
|
10821
|
+
}
|
|
10822
|
+
for (const value of Object.values(moduleExports)) {
|
|
10823
|
+
if (isSchemaObject(value)) {
|
|
10824
|
+
for (const group of Object.values(value)) {
|
|
10825
|
+
if (isRecord(group)) {
|
|
10826
|
+
for (const entity of Object.values(group)) {
|
|
10827
|
+
collect(entity);
|
|
10828
|
+
}
|
|
10829
|
+
}
|
|
10830
|
+
}
|
|
10831
|
+
} else {
|
|
10832
|
+
collect(value);
|
|
10833
|
+
}
|
|
10834
|
+
}
|
|
10835
|
+
return data;
|
|
10836
|
+
}
|
|
10837
|
+
async function loadSchema(entryPath) {
|
|
10838
|
+
const { createJiti } = await import('jiti');
|
|
10839
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
10840
|
+
const { resolve } = await import('pathe');
|
|
10841
|
+
const entryAbs = resolve(entryPath);
|
|
10842
|
+
const entryMod = await jiti.import(entryAbs);
|
|
10843
|
+
return classifyExports(entryMod);
|
|
10844
|
+
}
|
|
10845
|
+
|
|
10846
|
+
function sortSchemaByPos$1(schema) {
|
|
10847
|
+
const entries = Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
|
|
10848
|
+
const posA = typeof a.pos === "number" ? a.pos : Infinity;
|
|
10849
|
+
const posB = typeof b.pos === "number" ? b.pos : Infinity;
|
|
10850
|
+
return posA - posB;
|
|
10851
|
+
});
|
|
10852
|
+
return Object.fromEntries(
|
|
10853
|
+
entries.map(([key, field]) => {
|
|
10854
|
+
const { id, ...rest } = field;
|
|
10855
|
+
if (rest.restrict_type === "") {
|
|
10856
|
+
delete rest.restrict_type;
|
|
10857
|
+
}
|
|
10858
|
+
return [key, rest];
|
|
10859
|
+
})
|
|
10860
|
+
);
|
|
10861
|
+
}
|
|
10862
|
+
function serializeComponent(component, options = {}) {
|
|
10863
|
+
const stripSet = options.includeGroupUuid ? new Set([...COMPONENT_STRIP_KEYS].filter((key) => key !== "component_group_uuid")) : COMPONENT_STRIP_KEYS;
|
|
10864
|
+
const clean = stripKeys(component, stripSet);
|
|
10865
|
+
if (clean.schema && typeof clean.schema === "object") {
|
|
10866
|
+
clean.schema = sortSchemaByPos$1(clean.schema);
|
|
10867
|
+
}
|
|
10868
|
+
const ordered = {};
|
|
10869
|
+
if (clean.name !== void 0) {
|
|
10870
|
+
ordered.name = clean.name;
|
|
10871
|
+
}
|
|
10872
|
+
if (clean.display_name !== void 0) {
|
|
10873
|
+
ordered.display_name = clean.display_name;
|
|
10874
|
+
}
|
|
10875
|
+
if (clean.is_root !== void 0) {
|
|
10876
|
+
ordered.is_root = clean.is_root;
|
|
10877
|
+
}
|
|
10878
|
+
if (clean.is_nestable !== void 0) {
|
|
10879
|
+
ordered.is_nestable = clean.is_nestable;
|
|
10880
|
+
}
|
|
10881
|
+
const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
|
|
10882
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
10883
|
+
if (!handled.has(key)) {
|
|
10884
|
+
ordered[key] = value;
|
|
10885
|
+
}
|
|
10886
|
+
}
|
|
10887
|
+
if (clean.schema !== void 0) {
|
|
10888
|
+
ordered.schema = clean.schema;
|
|
10889
|
+
}
|
|
10890
|
+
return `defineBlock(${formatValue(ordered, 0)})`;
|
|
10891
|
+
}
|
|
10892
|
+
function serializeDatasource(datasource) {
|
|
10893
|
+
const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
|
|
10894
|
+
if (Array.isArray(clean.dimensions)) {
|
|
10895
|
+
clean.dimensions = clean.dimensions.map(
|
|
10896
|
+
(dim) => typeof dim === "object" && dim !== null ? stripKeys(dim, DATASOURCE_DIMENSION_STRIP_KEYS) : dim
|
|
10897
|
+
);
|
|
10898
|
+
}
|
|
10899
|
+
const ordered = {};
|
|
10900
|
+
if (clean.name !== void 0) {
|
|
10901
|
+
ordered.name = clean.name;
|
|
10902
|
+
}
|
|
10903
|
+
if (clean.slug !== void 0) {
|
|
10904
|
+
ordered.slug = clean.slug;
|
|
10905
|
+
}
|
|
10906
|
+
const handled = /* @__PURE__ */ new Set(["name", "slug"]);
|
|
10907
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
10908
|
+
if (!handled.has(key)) {
|
|
10909
|
+
ordered[key] = value;
|
|
10910
|
+
}
|
|
10911
|
+
}
|
|
10912
|
+
return `defineDatasource(${formatValue(ordered, 0)})`;
|
|
10913
|
+
}
|
|
10914
|
+
|
|
10915
|
+
function diffEntity(type, name, localSerialized, remoteSerialized) {
|
|
10916
|
+
if (!remoteSerialized && localSerialized) {
|
|
10917
|
+
return { type, name, action: "create", diff: null, local: null, remote: null };
|
|
10918
|
+
}
|
|
10919
|
+
if (remoteSerialized && !localSerialized) {
|
|
10920
|
+
return { type, name, action: "stale", diff: null, local: null, remote: null };
|
|
10921
|
+
}
|
|
10922
|
+
if (localSerialized === remoteSerialized) {
|
|
10923
|
+
return { type, name, action: "unchanged", diff: null, local: null, remote: null };
|
|
10924
|
+
}
|
|
10925
|
+
const patch = createTwoFilesPatch(
|
|
10926
|
+
`remote/${name}`,
|
|
10927
|
+
`local/${name}`,
|
|
10928
|
+
remoteSerialized,
|
|
10929
|
+
localSerialized,
|
|
10930
|
+
"remote",
|
|
10931
|
+
"local"
|
|
10932
|
+
);
|
|
10933
|
+
return { type, name, action: "update", diff: patch, local: null, remote: null };
|
|
10934
|
+
}
|
|
10935
|
+
function diffSchema(local, remote) {
|
|
10936
|
+
const diffs = [];
|
|
10937
|
+
const processedComponentNames = /* @__PURE__ */ new Set();
|
|
10938
|
+
for (const comp of local.components) {
|
|
10939
|
+
processedComponentNames.add(comp.name);
|
|
10940
|
+
const remoteComp = remote.components.get(comp.name);
|
|
10941
|
+
const includeGroupUuid = typeof comp.component_group_uuid === "string";
|
|
10942
|
+
const localSerialized = serializeComponent(applyDefaults(comp, COMPONENT_DEFAULTS), { includeGroupUuid });
|
|
10943
|
+
const remoteSerialized = remoteComp ? serializeComponent(applyDefaults(remoteComp, COMPONENT_DEFAULTS), { includeGroupUuid }) : null;
|
|
10944
|
+
diffs.push(diffEntity("component", comp.name, localSerialized, remoteSerialized));
|
|
10945
|
+
}
|
|
10946
|
+
for (const [name] of remote.components) {
|
|
10947
|
+
if (!processedComponentNames.has(name)) {
|
|
10948
|
+
diffs.push(diffEntity("component", name, null, "stale"));
|
|
10949
|
+
}
|
|
10950
|
+
}
|
|
10951
|
+
const processedDatasourceNames = /* @__PURE__ */ new Set();
|
|
10952
|
+
for (const ds of local.datasources) {
|
|
10953
|
+
processedDatasourceNames.add(ds.name);
|
|
10954
|
+
const remoteDs = remote.datasources.get(ds.name);
|
|
10955
|
+
const localSerialized = serializeDatasource(applyDefaults(ds, DATASOURCE_DEFAULTS));
|
|
10956
|
+
const remoteSerialized = remoteDs ? serializeDatasource(applyDefaults(remoteDs, DATASOURCE_DEFAULTS)) : null;
|
|
10957
|
+
diffs.push(diffEntity("datasource", ds.name, localSerialized, remoteSerialized));
|
|
10958
|
+
}
|
|
10959
|
+
for (const [name] of remote.datasources) {
|
|
10960
|
+
if (!processedDatasourceNames.has(name)) {
|
|
10961
|
+
diffs.push(diffEntity("datasource", name, null, "stale"));
|
|
10962
|
+
}
|
|
10963
|
+
}
|
|
10964
|
+
return {
|
|
10965
|
+
diffs,
|
|
10966
|
+
creates: diffs.filter((d) => d.action === "create").length,
|
|
10967
|
+
updates: diffs.filter((d) => d.action === "update").length,
|
|
10968
|
+
unchanged: diffs.filter((d) => d.action === "unchanged").length,
|
|
10969
|
+
stale: diffs.filter((d) => d.action === "stale").length
|
|
10970
|
+
};
|
|
10971
|
+
}
|
|
10972
|
+
|
|
10973
|
+
async function fetchRemoteSchema(spaceId) {
|
|
10974
|
+
const client = getMapiClient();
|
|
10975
|
+
const spaceIdNum = Number(spaceId);
|
|
10976
|
+
const [componentsRes, foldersRes, rawDatasources] = await Promise.all([
|
|
10977
|
+
client.components.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
|
|
10978
|
+
client.componentFolders.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
|
|
10979
|
+
fetchAllPages(
|
|
10980
|
+
(page) => client.datasources.list({ path: { space_id: spaceIdNum }, query: { page }, throwOnError: true }),
|
|
10981
|
+
(data) => data?.datasources ?? []
|
|
10982
|
+
)
|
|
10983
|
+
]);
|
|
10984
|
+
const rawComponents = componentsRes.data?.components ?? [];
|
|
10985
|
+
const rawComponentFolders = foldersRes.data?.component_groups ?? [];
|
|
10986
|
+
const remote = {
|
|
10987
|
+
components: new Map(rawComponents.map((c) => [c.name, c])),
|
|
10988
|
+
componentFolders: new Map(rawComponentFolders.map((f) => [f.name, f])),
|
|
10989
|
+
datasources: new Map(rawDatasources.map((d) => [d.name, d]))
|
|
10990
|
+
};
|
|
10991
|
+
return { remote, rawComponents, rawComponentFolders, rawDatasources };
|
|
10992
|
+
}
|
|
10993
|
+
|
|
10994
|
+
function isSchemaField(value) {
|
|
10995
|
+
return isRecord(value) && "type" in value;
|
|
10996
|
+
}
|
|
10997
|
+
function toSchemaRecord(schema) {
|
|
10998
|
+
const result = {};
|
|
10999
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
11000
|
+
if (key === "_uid" || key === "component" || !isSchemaField(value)) {
|
|
11001
|
+
continue;
|
|
11002
|
+
}
|
|
11003
|
+
result[key] = value;
|
|
11004
|
+
}
|
|
11005
|
+
return result;
|
|
11006
|
+
}
|
|
11007
|
+
function buildComponentPayload(input) {
|
|
11008
|
+
if (!isRecord(input)) {
|
|
11009
|
+
return { name: "" };
|
|
11010
|
+
}
|
|
11011
|
+
return {
|
|
11012
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
11013
|
+
// Fields in COMPONENT_DEFAULTS are always sent with their reset value so that
|
|
11014
|
+
// removing a field from the local schema actually clears it on the API.
|
|
11015
|
+
// (Root-level fields are additive on MAPI update — omitting preserves the old value.)
|
|
11016
|
+
display_name: typeof input.display_name === "string" ? input.display_name : "",
|
|
11017
|
+
description: typeof input.description === "string" ? input.description : "",
|
|
11018
|
+
color: typeof input.color === "string" ? input.color : "",
|
|
11019
|
+
icon: typeof input.icon === "string" ? input.icon : "",
|
|
11020
|
+
preview_field: typeof input.preview_field === "string" ? input.preview_field : "",
|
|
11021
|
+
internal_tag_ids: Array.isArray(input.internal_tag_ids) ? input.internal_tag_ids : [],
|
|
11022
|
+
// Conditionally sent: only included when explicitly set in local schema
|
|
11023
|
+
...isRecord(input.schema) && { schema: toSchemaRecord(input.schema) },
|
|
11024
|
+
...typeof input.is_root === "boolean" && { is_root: input.is_root },
|
|
11025
|
+
...typeof input.is_nestable === "boolean" && { is_nestable: input.is_nestable },
|
|
11026
|
+
...typeof input.component_group_uuid === "string" && { component_group_uuid: input.component_group_uuid }
|
|
11027
|
+
};
|
|
11028
|
+
}
|
|
11029
|
+
function toComponentCreate(input) {
|
|
11030
|
+
return buildComponentPayload(input);
|
|
11031
|
+
}
|
|
11032
|
+
function toComponentUpdate(input) {
|
|
11033
|
+
return buildComponentPayload(input);
|
|
11034
|
+
}
|
|
11035
|
+
function toDatasourceCreate(input) {
|
|
11036
|
+
if (!isRecord(input)) {
|
|
11037
|
+
return { name: "", slug: "" };
|
|
11038
|
+
}
|
|
11039
|
+
const result = {
|
|
11040
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
11041
|
+
slug: typeof input.slug === "string" ? input.slug : ""
|
|
11042
|
+
};
|
|
11043
|
+
if (Array.isArray(input.dimensions)) {
|
|
11044
|
+
result.dimensions_attributes = input.dimensions.filter((d) => isRecord(d) && typeof d.name === "string" && typeof d.entry_value === "string").map((d) => ({
|
|
11045
|
+
name: d.name,
|
|
11046
|
+
entry_value: d.entry_value
|
|
11047
|
+
}));
|
|
11048
|
+
}
|
|
11049
|
+
return result;
|
|
11050
|
+
}
|
|
11051
|
+
function toDatasourceUpdate(input, remote) {
|
|
11052
|
+
const base = toDatasourceCreate(input);
|
|
11053
|
+
const localDims = base.dimensions_attributes ?? [];
|
|
11054
|
+
const remoteDims = remote.dimensions ?? [];
|
|
11055
|
+
if (remoteDims.length === 0) {
|
|
11056
|
+
return base;
|
|
11057
|
+
}
|
|
11058
|
+
const localKeys = new Set(localDims.map((d) => `${d.name}::${d.entry_value}`));
|
|
11059
|
+
const destroyEntries = remoteDims.filter((rd) => rd.id != null && !localKeys.has(`${rd.name}::${rd.entry_value}`)).map((rd) => ({ id: rd.id, _destroy: true }));
|
|
11060
|
+
if (destroyEntries.length > 0) {
|
|
11061
|
+
return {
|
|
11062
|
+
...base,
|
|
11063
|
+
dimensions_attributes: [...localDims, ...destroyEntries]
|
|
11064
|
+
};
|
|
11065
|
+
}
|
|
11066
|
+
return base;
|
|
11067
|
+
}
|
|
11068
|
+
|
|
11069
|
+
function formatDiffOutput(result, options) {
|
|
11070
|
+
const lines = [];
|
|
11071
|
+
const byType = {
|
|
11072
|
+
component: [],
|
|
11073
|
+
datasource: []
|
|
11074
|
+
};
|
|
11075
|
+
for (const diff of result.diffs) {
|
|
11076
|
+
byType[diff.type].push(diff);
|
|
11077
|
+
}
|
|
11078
|
+
const willDelete = options?.delete ?? false;
|
|
11079
|
+
const icons = {
|
|
11080
|
+
create: chalk.green("+"),
|
|
11081
|
+
update: chalk.yellow("~"),
|
|
11082
|
+
unchanged: chalk.dim("="),
|
|
11083
|
+
stale: chalk.red("-")
|
|
11084
|
+
};
|
|
11085
|
+
const sections = [
|
|
11086
|
+
["Components", byType.component],
|
|
11087
|
+
["Datasources", byType.datasource]
|
|
11088
|
+
];
|
|
11089
|
+
for (const [label, diffs] of sections) {
|
|
11090
|
+
if (diffs.length === 0) {
|
|
11091
|
+
continue;
|
|
11092
|
+
}
|
|
11093
|
+
lines.push(chalk.bold(label));
|
|
11094
|
+
for (const diff of diffs) {
|
|
11095
|
+
const icon = icons[diff.action] ?? " ";
|
|
11096
|
+
const name = diff.action === "stale" ? chalk.red(diff.name) : diff.name;
|
|
11097
|
+
const actionLabel = diff.action === "stale" && willDelete ? "delete" : diff.action;
|
|
11098
|
+
lines.push(` ${icon} ${name} ${chalk.dim(`(${actionLabel})`)}`);
|
|
11099
|
+
if (diff.diff) {
|
|
11100
|
+
for (const line of diff.diff.split("\n")) {
|
|
11101
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
11102
|
+
lines.push(` ${chalk.green(line)}`);
|
|
11103
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
11104
|
+
lines.push(` ${chalk.red(line)}`);
|
|
11105
|
+
}
|
|
11106
|
+
}
|
|
11107
|
+
}
|
|
11108
|
+
}
|
|
11109
|
+
lines.push("");
|
|
11110
|
+
}
|
|
11111
|
+
const summary = [
|
|
11112
|
+
result.creates > 0 ? chalk.green(`${result.creates} to create`) : null,
|
|
11113
|
+
result.updates > 0 ? chalk.yellow(`${result.updates} to update`) : null,
|
|
11114
|
+
result.unchanged > 0 ? chalk.dim(`${result.unchanged} unchanged`) : null,
|
|
11115
|
+
result.stale > 0 ? chalk.red(`${result.stale} ${willDelete ? "to delete" : "stale"}`) : null
|
|
11116
|
+
].filter(Boolean).join(", ");
|
|
11117
|
+
lines.push(`Summary: ${summary}`);
|
|
11118
|
+
return lines.join("\n");
|
|
11119
|
+
}
|
|
11120
|
+
async function executePush(spaceId, local, remote, diffResult, options) {
|
|
11121
|
+
const client = getMapiClient();
|
|
11122
|
+
const spaceIdNum = Number(spaceId);
|
|
11123
|
+
let created = 0;
|
|
11124
|
+
let updated = 0;
|
|
11125
|
+
let deleted = 0;
|
|
11126
|
+
const componentDiffs = diffResult.diffs.filter((d) => d.type === "component");
|
|
11127
|
+
const componentResults = await Promise.allSettled(
|
|
11128
|
+
componentDiffs.map(async (diff) => {
|
|
11129
|
+
const localComp = local.components.find((c) => c.name === diff.name);
|
|
11130
|
+
if (diff.action === "create" && localComp) {
|
|
11131
|
+
await client.components.create({
|
|
11132
|
+
path: { space_id: spaceIdNum },
|
|
11133
|
+
body: { component: toComponentCreate(localComp) },
|
|
11134
|
+
throwOnError: true
|
|
11135
|
+
});
|
|
11136
|
+
return "created";
|
|
11137
|
+
}
|
|
11138
|
+
if (diff.action === "update" && localComp) {
|
|
11139
|
+
const existing = remote.components.get(diff.name);
|
|
11140
|
+
if (existing?.id) {
|
|
11141
|
+
await client.components.update(existing.id, {
|
|
11142
|
+
path: { space_id: spaceIdNum },
|
|
11143
|
+
body: { component: toComponentUpdate(localComp) },
|
|
11144
|
+
throwOnError: true
|
|
11145
|
+
});
|
|
11146
|
+
return "updated";
|
|
11147
|
+
}
|
|
11148
|
+
}
|
|
11149
|
+
})
|
|
11150
|
+
);
|
|
11151
|
+
for (let i = 0; i < componentResults.length; i++) {
|
|
11152
|
+
const result = componentResults[i];
|
|
11153
|
+
const diff = componentDiffs[i];
|
|
11154
|
+
if (result.status === "fulfilled") {
|
|
11155
|
+
if (result.value === "created") {
|
|
11156
|
+
created++;
|
|
11157
|
+
} else if (result.value === "updated") {
|
|
11158
|
+
updated++;
|
|
11159
|
+
}
|
|
11160
|
+
} else {
|
|
11161
|
+
const eventId = diff.action === "create" ? "push_component" : "update_component";
|
|
11162
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} component ${diff.name}`);
|
|
11163
|
+
}
|
|
11164
|
+
}
|
|
11165
|
+
const datasourceDiffs = diffResult.diffs.filter((d) => d.type === "datasource");
|
|
11166
|
+
const datasourceResults = await Promise.allSettled(
|
|
11167
|
+
datasourceDiffs.map(async (diff) => {
|
|
11168
|
+
const localDs = local.datasources.find((d) => d.name === diff.name);
|
|
11169
|
+
if (diff.action === "create" && localDs) {
|
|
11170
|
+
await client.datasources.create({
|
|
11171
|
+
path: { space_id: spaceIdNum },
|
|
11172
|
+
body: { datasource: toDatasourceCreate(localDs) },
|
|
11173
|
+
throwOnError: true
|
|
11174
|
+
});
|
|
11175
|
+
return "created";
|
|
11176
|
+
}
|
|
11177
|
+
if (diff.action === "update" && localDs) {
|
|
11178
|
+
const existing = remote.datasources.get(diff.name);
|
|
11179
|
+
if (existing?.id) {
|
|
11180
|
+
await client.datasources.update(existing.id, {
|
|
11181
|
+
path: { space_id: spaceIdNum },
|
|
11182
|
+
body: { datasource: toDatasourceUpdate(localDs, existing) },
|
|
11183
|
+
throwOnError: true
|
|
11184
|
+
});
|
|
11185
|
+
return "updated";
|
|
11186
|
+
}
|
|
11187
|
+
}
|
|
11188
|
+
})
|
|
11189
|
+
);
|
|
11190
|
+
for (let i = 0; i < datasourceResults.length; i++) {
|
|
11191
|
+
const result = datasourceResults[i];
|
|
11192
|
+
const diff = datasourceDiffs[i];
|
|
11193
|
+
if (result.status === "fulfilled") {
|
|
11194
|
+
if (result.value === "created") {
|
|
11195
|
+
created++;
|
|
11196
|
+
} else if (result.value === "updated") {
|
|
11197
|
+
updated++;
|
|
11198
|
+
}
|
|
11199
|
+
} else {
|
|
11200
|
+
const eventId = diff.action === "create" ? "push_datasource" : "update_datasource";
|
|
11201
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} datasource ${diff.name}`);
|
|
11202
|
+
}
|
|
11203
|
+
}
|
|
11204
|
+
if (options.delete) {
|
|
11205
|
+
const staleComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
|
|
11206
|
+
const deleteComponentResults = await Promise.allSettled(
|
|
11207
|
+
staleComponents.map(async (diff) => {
|
|
11208
|
+
const existing = remote.components.get(diff.name);
|
|
11209
|
+
if (existing?.id) {
|
|
11210
|
+
await client.components.delete(existing.id, {
|
|
11211
|
+
path: { space_id: spaceIdNum },
|
|
11212
|
+
throwOnError: true
|
|
11213
|
+
});
|
|
11214
|
+
return true;
|
|
11215
|
+
}
|
|
11216
|
+
})
|
|
11217
|
+
);
|
|
11218
|
+
for (let i = 0; i < deleteComponentResults.length; i++) {
|
|
11219
|
+
const result = deleteComponentResults[i];
|
|
11220
|
+
if (result.status === "fulfilled") {
|
|
11221
|
+
if (result.value) {
|
|
11222
|
+
deleted++;
|
|
11223
|
+
}
|
|
11224
|
+
} else {
|
|
11225
|
+
handleAPIError("delete_component", result.reason, `Failed to delete component ${staleComponents[i].name}`);
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
11228
|
+
const staleDatasources = diffResult.diffs.filter((d) => d.type === "datasource" && d.action === "stale");
|
|
11229
|
+
const deleteDatasourceResults = await Promise.allSettled(
|
|
11230
|
+
staleDatasources.map(async (diff) => {
|
|
11231
|
+
const existing = remote.datasources.get(diff.name);
|
|
11232
|
+
if (existing?.id) {
|
|
11233
|
+
await client.datasources.delete(existing.id, {
|
|
11234
|
+
path: { space_id: spaceIdNum },
|
|
11235
|
+
throwOnError: true
|
|
11236
|
+
});
|
|
11237
|
+
return true;
|
|
11238
|
+
}
|
|
11239
|
+
})
|
|
11240
|
+
);
|
|
11241
|
+
for (let i = 0; i < deleteDatasourceResults.length; i++) {
|
|
11242
|
+
const result = deleteDatasourceResults[i];
|
|
11243
|
+
if (result.status === "fulfilled") {
|
|
11244
|
+
if (result.value) {
|
|
11245
|
+
deleted++;
|
|
11246
|
+
}
|
|
11247
|
+
} else {
|
|
11248
|
+
handleAPIError("delete_datasource", result.reason, `Failed to delete datasource ${staleDatasources[i].name}`);
|
|
11249
|
+
}
|
|
11250
|
+
}
|
|
11251
|
+
}
|
|
11252
|
+
return { created, updated, deleted };
|
|
11253
|
+
}
|
|
11254
|
+
function buildChangesetEntries(diffResult, local, remote, options) {
|
|
11255
|
+
const changes = [];
|
|
11256
|
+
for (const diff of diffResult.diffs) {
|
|
11257
|
+
if (diff.action === "unchanged") {
|
|
11258
|
+
continue;
|
|
11259
|
+
}
|
|
11260
|
+
if (diff.action === "stale" && !options.delete) {
|
|
11261
|
+
continue;
|
|
11262
|
+
}
|
|
11263
|
+
const action = diff.action === "stale" ? "delete" : diff.action;
|
|
11264
|
+
let remoteSrc;
|
|
11265
|
+
let localSrc;
|
|
11266
|
+
if (diff.type === "component") {
|
|
11267
|
+
remoteSrc = remote.components.get(diff.name);
|
|
11268
|
+
localSrc = local.components.find((c) => c.name === diff.name);
|
|
11269
|
+
} else if (diff.type === "datasource") {
|
|
11270
|
+
remoteSrc = remote.datasources.get(diff.name);
|
|
11271
|
+
localSrc = local.datasources.find((d) => d.name === diff.name);
|
|
11272
|
+
}
|
|
11273
|
+
changes.push({
|
|
11274
|
+
type: diff.type,
|
|
11275
|
+
name: diff.name,
|
|
11276
|
+
action,
|
|
11277
|
+
...remoteSrc && { before: { ...remoteSrc } },
|
|
11278
|
+
...localSrc && { after: { ...localSrc } }
|
|
11279
|
+
});
|
|
11280
|
+
}
|
|
11281
|
+
return changes;
|
|
11282
|
+
}
|
|
11283
|
+
|
|
11284
|
+
async function ensureDir(dir) {
|
|
11285
|
+
await mkdir(dir, { recursive: true });
|
|
11286
|
+
}
|
|
11287
|
+
async function saveChangeset(basePath, data) {
|
|
11288
|
+
const dir = join(basePath, "schema", "changesets");
|
|
11289
|
+
await ensureDir(dir);
|
|
11290
|
+
const fileName = `${fileTimestamp(data.timestamp)}.json`;
|
|
11291
|
+
const filePath = join(dir, fileName);
|
|
11292
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
11293
|
+
return filePath;
|
|
11294
|
+
}
|
|
11295
|
+
|
|
11296
|
+
const SENTINEL_FIELDS = /* @__PURE__ */ new Set(["_uid", "component"]);
|
|
11297
|
+
function classifyFieldChanges(remoteSchema, localSchema) {
|
|
11298
|
+
const removed = [];
|
|
11299
|
+
const added = [];
|
|
11300
|
+
const typeChanged = [];
|
|
11301
|
+
const requiredAdded = [];
|
|
11302
|
+
const requiredChanged = [];
|
|
11303
|
+
for (const [field, remoteField] of Object.entries(remoteSchema)) {
|
|
11304
|
+
if (SENTINEL_FIELDS.has(field)) {
|
|
11305
|
+
continue;
|
|
11306
|
+
}
|
|
11307
|
+
if (typeof remoteField.type !== "string") {
|
|
11308
|
+
continue;
|
|
11309
|
+
}
|
|
11310
|
+
if (!(field in localSchema)) {
|
|
11311
|
+
removed.push({ field, type: remoteField.type });
|
|
11312
|
+
}
|
|
11313
|
+
}
|
|
11314
|
+
for (const [field, localField] of Object.entries(localSchema)) {
|
|
11315
|
+
if (SENTINEL_FIELDS.has(field)) {
|
|
11316
|
+
continue;
|
|
11317
|
+
}
|
|
11318
|
+
if (typeof localField.type !== "string") {
|
|
11319
|
+
continue;
|
|
11320
|
+
}
|
|
11321
|
+
if (!(field in remoteSchema)) {
|
|
11322
|
+
if (localField.required) {
|
|
11323
|
+
requiredAdded.push({ field, type: localField.type });
|
|
11324
|
+
} else {
|
|
11325
|
+
added.push({ field, type: localField.type, required: false });
|
|
11326
|
+
}
|
|
11327
|
+
} else {
|
|
11328
|
+
const remoteField = remoteSchema[field];
|
|
11329
|
+
if (typeof remoteField?.type !== "string") {
|
|
11330
|
+
continue;
|
|
11331
|
+
}
|
|
11332
|
+
if (remoteField.type !== localField.type) {
|
|
11333
|
+
typeChanged.push({ field, oldType: remoteField.type, newType: localField.type });
|
|
11334
|
+
}
|
|
11335
|
+
if (localField.required && !remoteField.required) {
|
|
11336
|
+
requiredChanged.push({ field, type: localField.type });
|
|
11337
|
+
}
|
|
11338
|
+
}
|
|
11339
|
+
}
|
|
11340
|
+
return { removed, added, typeChanged, requiredAdded, requiredChanged };
|
|
11341
|
+
}
|
|
11342
|
+
function longestCommonSubstring(a, b) {
|
|
11343
|
+
let maxLen = 0;
|
|
11344
|
+
for (let i = 0; i < a.length; i++) {
|
|
11345
|
+
for (let j = 0; j < b.length; j++) {
|
|
11346
|
+
let len = 0;
|
|
11347
|
+
while (i + len < a.length && j + len < b.length && a[i + len] === b[j + len]) {
|
|
11348
|
+
len++;
|
|
11349
|
+
}
|
|
11350
|
+
if (len > maxLen) {
|
|
11351
|
+
maxLen = len;
|
|
11352
|
+
}
|
|
11353
|
+
}
|
|
11354
|
+
}
|
|
11355
|
+
return maxLen;
|
|
11356
|
+
}
|
|
11357
|
+
function nameSimilarity(a, b) {
|
|
11358
|
+
const longer = Math.max(a.length, b.length);
|
|
11359
|
+
if (longer === 0) {
|
|
11360
|
+
return 1;
|
|
11361
|
+
}
|
|
11362
|
+
return longestCommonSubstring(a, b) / longer;
|
|
11363
|
+
}
|
|
11364
|
+
function detectRenames(removed, added) {
|
|
11365
|
+
const renames = [];
|
|
11366
|
+
const usedRemoved = /* @__PURE__ */ new Set();
|
|
11367
|
+
const usedAdded = /* @__PURE__ */ new Set();
|
|
11368
|
+
const addedByType = /* @__PURE__ */ new Map();
|
|
11369
|
+
for (const addedField of added) {
|
|
11370
|
+
if (!addedByType.has(addedField.type)) {
|
|
11371
|
+
addedByType.set(addedField.type, []);
|
|
11372
|
+
}
|
|
11373
|
+
addedByType.get(addedField.type).push(addedField);
|
|
11374
|
+
}
|
|
11375
|
+
const isSinglePair = removed.length === 1 && added.length === 1;
|
|
11376
|
+
for (const removedField of removed) {
|
|
11377
|
+
const candidates = addedByType.get(removedField.type) ?? [];
|
|
11378
|
+
const availableCandidates = candidates.filter((c) => !usedAdded.has(c.field));
|
|
11379
|
+
if (availableCandidates.length === 0) {
|
|
11380
|
+
continue;
|
|
11381
|
+
}
|
|
11382
|
+
let bestCandidate = availableCandidates[0];
|
|
11383
|
+
let bestScore = nameSimilarity(removedField.field, bestCandidate.field);
|
|
11384
|
+
for (let i = 1; i < availableCandidates.length; i++) {
|
|
11385
|
+
const score = nameSimilarity(removedField.field, availableCandidates[i].field);
|
|
11386
|
+
if (score > bestScore) {
|
|
11387
|
+
bestScore = score;
|
|
11388
|
+
bestCandidate = availableCandidates[i];
|
|
11389
|
+
}
|
|
11390
|
+
}
|
|
11391
|
+
if (!isSinglePair && bestScore < 0.3) {
|
|
11392
|
+
continue;
|
|
11393
|
+
}
|
|
11394
|
+
renames.push({ oldField: removedField.field, newField: bestCandidate.field, fieldType: removedField.type });
|
|
11395
|
+
usedRemoved.add(removedField.field);
|
|
11396
|
+
usedAdded.add(bestCandidate.field);
|
|
11397
|
+
}
|
|
11398
|
+
const unmatchedRemoved = removed.filter((r) => !usedRemoved.has(r.field));
|
|
11399
|
+
const unmatchedAdded = added.filter((a) => !usedAdded.has(a.field));
|
|
11400
|
+
return { renames, unmatchedRemoved, unmatchedAdded };
|
|
11401
|
+
}
|
|
11402
|
+
function analyzeBreakingChanges(diffResult, local, remote) {
|
|
11403
|
+
const results = [];
|
|
11404
|
+
const updatedComponents = diffResult.diffs.filter(
|
|
11405
|
+
(d) => d.type === "component" && d.action === "update"
|
|
11406
|
+
);
|
|
11407
|
+
for (const diff of updatedComponents) {
|
|
11408
|
+
const localComp = local.components.find((c) => c.name === diff.name);
|
|
11409
|
+
const remoteComp = remote.components.get(diff.name);
|
|
11410
|
+
if (!localComp?.schema || !remoteComp?.schema) {
|
|
11411
|
+
continue;
|
|
11412
|
+
}
|
|
11413
|
+
const classification = classifyFieldChanges(
|
|
11414
|
+
remoteComp.schema,
|
|
11415
|
+
localComp.schema
|
|
11416
|
+
);
|
|
11417
|
+
const changes = [];
|
|
11418
|
+
const { renames, unmatchedRemoved } = detectRenames(classification.removed, classification.added);
|
|
11419
|
+
for (const rename of renames) {
|
|
11420
|
+
changes.push({ kind: "rename", field: rename.newField, oldField: rename.oldField });
|
|
11421
|
+
}
|
|
11422
|
+
for (const removed of unmatchedRemoved) {
|
|
11423
|
+
changes.push({ kind: "removed", field: removed.field });
|
|
11424
|
+
}
|
|
11425
|
+
for (const tc of classification.typeChanged) {
|
|
11426
|
+
changes.push({ kind: "type_changed", field: tc.field, oldType: tc.oldType, newType: tc.newType });
|
|
11427
|
+
}
|
|
11428
|
+
for (const ra of classification.requiredAdded) {
|
|
11429
|
+
changes.push({ kind: "required_added", field: ra.field, fieldType: ra.type });
|
|
11430
|
+
}
|
|
11431
|
+
for (const rc of classification.requiredChanged) {
|
|
11432
|
+
changes.push({ kind: "required_changed", field: rc.field, fieldType: rc.type });
|
|
11433
|
+
}
|
|
11434
|
+
if (changes.length > 0) {
|
|
11435
|
+
results.push({ componentName: diff.name, changes });
|
|
11436
|
+
}
|
|
11437
|
+
}
|
|
11438
|
+
return results;
|
|
11439
|
+
}
|
|
11440
|
+
|
|
11441
|
+
const COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["text:textarea", "textarea:text"]);
|
|
11442
|
+
function defaultForType(fieldType) {
|
|
11443
|
+
switch (fieldType) {
|
|
11444
|
+
case "text":
|
|
11445
|
+
case "textarea":
|
|
11446
|
+
case "markdown":
|
|
11447
|
+
return `''`;
|
|
11448
|
+
case "number":
|
|
11449
|
+
return "0";
|
|
11450
|
+
case "boolean":
|
|
11451
|
+
return "false";
|
|
11452
|
+
default:
|
|
11453
|
+
return null;
|
|
11454
|
+
}
|
|
11455
|
+
}
|
|
11456
|
+
function typeConversion(field, oldType, newType) {
|
|
11457
|
+
const key = `${oldType}:${newType}`;
|
|
11458
|
+
if (COMPATIBLE_TYPES.has(key)) {
|
|
11459
|
+
return null;
|
|
11460
|
+
}
|
|
11461
|
+
const accessor = `block.${field}`;
|
|
11462
|
+
switch (key) {
|
|
11463
|
+
case "text:number":
|
|
11464
|
+
return `${accessor} = Number(${accessor}) || 0;`;
|
|
11465
|
+
case "number:text":
|
|
11466
|
+
return `${accessor} = String(${accessor});`;
|
|
11467
|
+
case "text:boolean":
|
|
11468
|
+
return `${accessor} = !!${accessor};`;
|
|
11469
|
+
case "boolean:text":
|
|
11470
|
+
return `${accessor} = String(${accessor});`;
|
|
11471
|
+
default:
|
|
11472
|
+
return `${accessor}; // TODO: convert from ${oldType} to ${newType}`;
|
|
11473
|
+
}
|
|
11474
|
+
}
|
|
11475
|
+
function renderMigrationCode(changes) {
|
|
11476
|
+
const lines = [];
|
|
11477
|
+
lines.push(" // Review this migration before running it against your space.");
|
|
11478
|
+
lines.push(" // Generated migrations are scaffolds and may need manual adjustments.");
|
|
11479
|
+
lines.push(" // Example rename migration:");
|
|
11480
|
+
lines.push(" // block.new_field = block.old_field;");
|
|
11481
|
+
lines.push(" // delete block.old_field;");
|
|
11482
|
+
lines.push("");
|
|
11483
|
+
for (const change of changes) {
|
|
11484
|
+
switch (change.kind) {
|
|
11485
|
+
case "rename":
|
|
11486
|
+
lines.push(` // Rename: ${change.oldField} \u2192 ${change.field}`);
|
|
11487
|
+
lines.push(` if ('${change.oldField}' in block) {`);
|
|
11488
|
+
lines.push(` block.${change.field} = block.${change.oldField};`);
|
|
11489
|
+
lines.push(` delete block.${change.oldField};`);
|
|
11490
|
+
lines.push(` }`);
|
|
11491
|
+
break;
|
|
11492
|
+
case "removed":
|
|
11493
|
+
if (change.renameHint) {
|
|
11494
|
+
lines.push(` // If '${change.field}' was renamed to '${change.renameHint.newField}', uncomment:`);
|
|
11495
|
+
lines.push(` // block.${change.renameHint.newField} = block.${change.field};`);
|
|
11496
|
+
} else {
|
|
11497
|
+
lines.push(` // Removed field: ${change.field}`);
|
|
11498
|
+
}
|
|
11499
|
+
lines.push(` delete block.${change.field};`);
|
|
11500
|
+
break;
|
|
11501
|
+
case "type_changed": {
|
|
11502
|
+
const conversion = typeConversion(change.field, change.oldType, change.newType);
|
|
11503
|
+
if (conversion) {
|
|
11504
|
+
lines.push(` // Type change: ${change.field} (${change.oldType} \u2192 ${change.newType})`);
|
|
11505
|
+
lines.push(` ${conversion}`);
|
|
11506
|
+
}
|
|
11507
|
+
break;
|
|
11508
|
+
}
|
|
11509
|
+
case "required_added": {
|
|
11510
|
+
const defaultValue = defaultForType(change.fieldType);
|
|
11511
|
+
lines.push(` // New required field: ${change.field} (${change.fieldType})`);
|
|
11512
|
+
if (defaultValue !== null) {
|
|
11513
|
+
lines.push(` // TODO: provide a meaningful default value`);
|
|
11514
|
+
lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
|
|
11515
|
+
} else {
|
|
11516
|
+
lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
|
|
11517
|
+
lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
|
|
11518
|
+
}
|
|
11519
|
+
break;
|
|
11520
|
+
}
|
|
11521
|
+
case "required_changed": {
|
|
11522
|
+
const defaultValue = defaultForType(change.fieldType);
|
|
11523
|
+
lines.push(` // Field is now required: ${change.field} (${change.fieldType})`);
|
|
11524
|
+
lines.push(` // Existing stories may have null/undefined values \u2014 provide a default for those.`);
|
|
11525
|
+
if (defaultValue !== null) {
|
|
11526
|
+
lines.push(` // TODO: provide a meaningful default value`);
|
|
11527
|
+
lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
|
|
11528
|
+
} else {
|
|
11529
|
+
lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
|
|
11530
|
+
lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
|
|
11531
|
+
}
|
|
11532
|
+
break;
|
|
11533
|
+
}
|
|
11534
|
+
}
|
|
11535
|
+
lines.push("");
|
|
11536
|
+
}
|
|
11537
|
+
const body = lines.length > 0 ? `
|
|
11538
|
+
${lines.join("\n")}` : "\n";
|
|
11539
|
+
return `export default function (block) {${body} return block;
|
|
11540
|
+
}
|
|
11541
|
+
`;
|
|
11542
|
+
}
|
|
11543
|
+
async function writeMigrationFile(options) {
|
|
11544
|
+
const { spaceId, componentName, code, timestamp, basePath } = options;
|
|
11545
|
+
const dir = resolvePath(basePath, `migrations/${spaceId}`);
|
|
11546
|
+
await mkdir(dir, { recursive: true });
|
|
11547
|
+
const fileName = `${componentName}.${fileTimestamp(timestamp)}.js`;
|
|
11548
|
+
const filePath = join(dir, fileName);
|
|
11549
|
+
await writeFile(filePath, code, "utf-8");
|
|
11550
|
+
return filePath;
|
|
11551
|
+
}
|
|
11552
|
+
|
|
11553
|
+
const DEFAULT_GROUPS_FILENAME = "groups.json";
|
|
11554
|
+
const CONSOLIDATED_COMPONENTS_FILENAME = "components.json";
|
|
11555
|
+
async function writeLocalComponents({
|
|
11556
|
+
space,
|
|
11557
|
+
basePath,
|
|
11558
|
+
resolved,
|
|
11559
|
+
diffResult,
|
|
11560
|
+
deleteRemoved,
|
|
11561
|
+
ui,
|
|
11562
|
+
logger
|
|
11563
|
+
}) {
|
|
11564
|
+
const componentsDir = resolveCommandPath(directories.components, space, basePath);
|
|
11565
|
+
const consolidatedPath = join(componentsDir, CONSOLIDATED_COMPONENTS_FILENAME);
|
|
11566
|
+
if (await fileExists(consolidatedPath)) {
|
|
11567
|
+
ui.warn(
|
|
11568
|
+
`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.`
|
|
11569
|
+
);
|
|
11570
|
+
}
|
|
11571
|
+
for (const component of resolved.components) {
|
|
11572
|
+
const filePath = join(componentsDir, `${sanitizeFilename(component.name || "")}.json`);
|
|
11573
|
+
await saveToFile(filePath, JSON.stringify(component, null, 2));
|
|
11574
|
+
}
|
|
11575
|
+
const groupsPath = join(componentsDir, DEFAULT_GROUPS_FILENAME);
|
|
11576
|
+
if (await fileExists(groupsPath)) {
|
|
11577
|
+
try {
|
|
11578
|
+
await unlink(groupsPath);
|
|
11579
|
+
logger.info("Removed stale local groups file", { path: displayPath(groupsPath, basePath) });
|
|
11580
|
+
} catch (error) {
|
|
11581
|
+
if (error.code !== "ENOENT") {
|
|
11582
|
+
throw error;
|
|
11583
|
+
}
|
|
11584
|
+
}
|
|
11585
|
+
}
|
|
11586
|
+
if (deleteRemoved) {
|
|
11587
|
+
const staleComponents = diffResult.diffs.filter(
|
|
11588
|
+
(d) => d.type === "component" && d.action === "stale"
|
|
11589
|
+
);
|
|
11590
|
+
for (const stale of staleComponents) {
|
|
11591
|
+
const filePath = join(componentsDir, `${sanitizeFilename(stale.name)}.json`);
|
|
11592
|
+
try {
|
|
11593
|
+
await unlink(filePath);
|
|
11594
|
+
logger.info("Removed stale local component file", { path: displayPath(filePath, basePath) });
|
|
11595
|
+
} catch (error) {
|
|
11596
|
+
if (error.code !== "ENOENT") {
|
|
11597
|
+
throw error;
|
|
11598
|
+
}
|
|
11599
|
+
}
|
|
11600
|
+
}
|
|
11601
|
+
}
|
|
11602
|
+
logger.info("Wrote local component files", {
|
|
11603
|
+
space,
|
|
11604
|
+
componentsWritten: resolved.components.length
|
|
11605
|
+
});
|
|
11606
|
+
}
|
|
11607
|
+
|
|
11608
|
+
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) => {
|
|
11609
|
+
const ui = getUI();
|
|
11610
|
+
const logger = getLogger();
|
|
11611
|
+
const reporter = getReporter();
|
|
11612
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
11613
|
+
const { state } = session();
|
|
11614
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Pushing schema...");
|
|
11615
|
+
logger.info("Schema push started", { entryFile, space });
|
|
11616
|
+
if (!requireAuthentication(state, verbose)) {
|
|
11617
|
+
return;
|
|
11618
|
+
}
|
|
11619
|
+
if (!space) {
|
|
11620
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
11621
|
+
return;
|
|
11622
|
+
}
|
|
11623
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
11624
|
+
try {
|
|
11625
|
+
const loadSpinner = ui.createSpinner("Resolving schema...");
|
|
11626
|
+
let local;
|
|
11627
|
+
try {
|
|
11628
|
+
local = await loadSchema(entryFile);
|
|
11629
|
+
} catch (maybeError) {
|
|
11630
|
+
loadSpinner.failed("Failed to resolve schema");
|
|
11631
|
+
handleError(toError(maybeError), verbose);
|
|
11632
|
+
return;
|
|
11633
|
+
}
|
|
11634
|
+
loadSpinner.succeed(`Found: ${local.components.length} components, ${local.datasources.length} datasources`);
|
|
11635
|
+
const totalLocal = local.components.length + local.datasources.length;
|
|
11636
|
+
if (totalLocal === 0) {
|
|
11637
|
+
ui.warn("No components or datasources found in the entry file. Verify the file exports schema definitions.");
|
|
11638
|
+
return;
|
|
11639
|
+
}
|
|
11640
|
+
const remoteSpinner = ui.createSpinner(`Fetching remote state from space ${space}...`);
|
|
11641
|
+
let remoteResult;
|
|
11642
|
+
try {
|
|
11643
|
+
remoteResult = await fetchRemoteSchema(space);
|
|
11644
|
+
} catch (maybeError) {
|
|
11645
|
+
remoteSpinner.failed("Failed to fetch remote schema");
|
|
11646
|
+
handleError(toError(maybeError), verbose);
|
|
11647
|
+
return;
|
|
11648
|
+
}
|
|
11649
|
+
const { remote, rawComponents, rawComponentFolders, rawDatasources } = remoteResult;
|
|
11650
|
+
remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.datasources.size} datasources`);
|
|
11651
|
+
const diffResult = diffSchema(local, remote);
|
|
11652
|
+
ui.br();
|
|
11653
|
+
ui.log(formatDiffOutput(diffResult, { delete: options.delete }));
|
|
11654
|
+
if (options.migrations) {
|
|
11655
|
+
const breakingChanges = analyzeBreakingChanges(diffResult, local, remote);
|
|
11656
|
+
if (breakingChanges.length > 0) {
|
|
11657
|
+
const totalChanges = breakingChanges.reduce((sum, c) => sum + c.changes.length, 0);
|
|
11658
|
+
ui.br();
|
|
11659
|
+
ui.warn(`${totalChanges} breaking change(s) detected in ${breakingChanges.length} component(s).`);
|
|
11660
|
+
ui.info("Generated migrations are scaffolds. Review and adjust them before running `storyblok migrations run`.");
|
|
11661
|
+
if (!options.dryRun) {
|
|
11662
|
+
const explicitMigrations = command.getOptionValueSource("migrations") === "cli";
|
|
11663
|
+
const shouldGenerate = explicitMigrations || await confirm({
|
|
11664
|
+
message: "Generate migration files for breaking changes?",
|
|
11665
|
+
default: true
|
|
11666
|
+
});
|
|
11667
|
+
if (shouldGenerate) {
|
|
11668
|
+
const migrationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11669
|
+
const resolvedBase = resolvePath(basePath, "");
|
|
11670
|
+
for (const comp of breakingChanges) {
|
|
11671
|
+
const renames = comp.changes.filter((c) => c.kind === "rename");
|
|
11672
|
+
if (renames.length > 0 && explicitMigrations) {
|
|
11673
|
+
for (const r of renames) {
|
|
11674
|
+
if (r.kind === "rename") {
|
|
11675
|
+
ui.log(` Assumed rename in '${comp.componentName}': ${r.oldField} \u2192 ${r.field}`);
|
|
11676
|
+
}
|
|
11677
|
+
}
|
|
11678
|
+
}
|
|
11679
|
+
if (renames.length > 0 && !explicitMigrations) {
|
|
11680
|
+
ui.br();
|
|
11681
|
+
ui.log(`Detected renames in '${comp.componentName}':`);
|
|
11682
|
+
for (const r of renames) {
|
|
11683
|
+
if (r.kind === "rename") {
|
|
11684
|
+
ui.log(` ${r.oldField} \u2192 ${r.field}`);
|
|
11685
|
+
}
|
|
11686
|
+
}
|
|
11687
|
+
const renameConfirmed = await confirm({
|
|
11688
|
+
message: "Are these renames correct?",
|
|
11689
|
+
default: true
|
|
11690
|
+
});
|
|
11691
|
+
if (!renameConfirmed) {
|
|
11692
|
+
comp.changes = comp.changes.map((c) => {
|
|
11693
|
+
if (c.kind === "rename") {
|
|
11694
|
+
return { kind: "removed", field: c.oldField, renameHint: { newField: c.field } };
|
|
11695
|
+
}
|
|
11696
|
+
return c;
|
|
11697
|
+
});
|
|
11698
|
+
}
|
|
11699
|
+
}
|
|
11700
|
+
const code = renderMigrationCode(comp.changes);
|
|
11701
|
+
const path = await writeMigrationFile({
|
|
11702
|
+
spaceId: space,
|
|
11703
|
+
componentName: comp.componentName,
|
|
11704
|
+
code,
|
|
11705
|
+
timestamp: migrationTimestamp,
|
|
11706
|
+
basePath: resolvedBase
|
|
11707
|
+
});
|
|
11708
|
+
const migrationPath = displayPath(path, basePath);
|
|
11709
|
+
logger.info("Migration generated", { component: comp.componentName, path: migrationPath });
|
|
11710
|
+
ui.log(` Generated: ${migrationPath}`);
|
|
11711
|
+
}
|
|
11712
|
+
ui.br();
|
|
11713
|
+
ui.info(`Run migrations when ready: storyblok migrations run --space ${space}`);
|
|
11714
|
+
}
|
|
11715
|
+
}
|
|
11716
|
+
}
|
|
11717
|
+
}
|
|
11718
|
+
if (options.delete) {
|
|
11719
|
+
const deletedComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
|
|
11720
|
+
for (const comp of deletedComponents) {
|
|
11721
|
+
ui.warn(`Component '${comp.name}' will be deleted. Stories using it will have out-of-schema content.`);
|
|
11722
|
+
}
|
|
11723
|
+
}
|
|
11724
|
+
if (diffResult.stale > 0 && !options.delete) {
|
|
11725
|
+
ui.warn(`${diffResult.stale} stale entity(s) exist remotely but not in schema. Use --delete to remove.`);
|
|
11726
|
+
}
|
|
11727
|
+
if (options.dryRun) {
|
|
11728
|
+
ui.info("Dry run \u2014 no changes applied.");
|
|
11729
|
+
logger.info("Dry run completed", { creates: diffResult.creates, updates: diffResult.updates });
|
|
11730
|
+
return;
|
|
11731
|
+
}
|
|
11732
|
+
const resolvedPath = resolvePath(basePath, "");
|
|
11733
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11734
|
+
const changesetPath = await saveChangeset(resolvedPath, {
|
|
11735
|
+
timestamp,
|
|
11736
|
+
spaceId: Number(space),
|
|
11737
|
+
remote: { components: rawComponents, componentFolders: rawComponentFolders, datasources: rawDatasources },
|
|
11738
|
+
changes: buildChangesetEntries(diffResult, local, remote, { delete: options.delete })
|
|
11739
|
+
});
|
|
11740
|
+
logger.info("Changeset saved", { path: displayPath(changesetPath, basePath) });
|
|
11741
|
+
const nothingToPush = diffResult.creates === 0 && diffResult.updates === 0 && (!options.delete || diffResult.stale === 0);
|
|
11742
|
+
if (nothingToPush) {
|
|
11743
|
+
ui.ok("Everything up to date \u2014 nothing to push.");
|
|
11744
|
+
} else {
|
|
11745
|
+
const pushSpinner = ui.createSpinner("Pushing schema...");
|
|
11746
|
+
let result;
|
|
11747
|
+
try {
|
|
11748
|
+
result = await executePush(space, local, remote, diffResult, { delete: options.delete });
|
|
11749
|
+
} catch (error) {
|
|
11750
|
+
pushSpinner.failed("Failed to push schema");
|
|
11751
|
+
throw error;
|
|
11752
|
+
}
|
|
11753
|
+
summary.total = result.created + result.updated + result.deleted;
|
|
11754
|
+
summary.succeeded = summary.total;
|
|
11755
|
+
pushSpinner.succeed(`Pushed ${result.created} creations, ${result.updated} updates${result.deleted > 0 ? `, ${result.deleted} deletions` : ""}.`);
|
|
11756
|
+
}
|
|
11757
|
+
if (options.writeComponents) {
|
|
11758
|
+
try {
|
|
11759
|
+
await writeLocalComponents({
|
|
11760
|
+
space,
|
|
11761
|
+
basePath,
|
|
11762
|
+
resolved: local,
|
|
11763
|
+
diffResult,
|
|
11764
|
+
deleteRemoved: options.delete,
|
|
11765
|
+
ui,
|
|
11766
|
+
logger
|
|
11767
|
+
});
|
|
11768
|
+
} catch (writeError) {
|
|
11769
|
+
ui.warn(`Failed to write local component files: ${toError(writeError).message}`);
|
|
11770
|
+
logger.warn("Failed to write local component files", { error: toError(writeError).message });
|
|
11771
|
+
}
|
|
11772
|
+
}
|
|
11773
|
+
} catch (maybeError) {
|
|
11774
|
+
summary.failed += 1;
|
|
11775
|
+
handleError(toError(maybeError), verbose);
|
|
11776
|
+
} finally {
|
|
11777
|
+
logger.info("Schema push finished", { summary });
|
|
11778
|
+
reporter.addSummary("schemaPushResults", summary);
|
|
11779
|
+
reporter.finalize();
|
|
11780
|
+
}
|
|
11781
|
+
});
|
|
11782
|
+
|
|
11783
|
+
function buildGroupPathByUuid(folders) {
|
|
11784
|
+
const byUuid = new Map(folders.map((folder) => [folder.uuid, folder]));
|
|
11785
|
+
const cache = /* @__PURE__ */ new Map();
|
|
11786
|
+
function pathFor(uuid, visited) {
|
|
11787
|
+
if (!uuid) {
|
|
11788
|
+
return [];
|
|
11789
|
+
}
|
|
11790
|
+
const cached = cache.get(uuid);
|
|
11791
|
+
if (cached) {
|
|
11792
|
+
return cached;
|
|
11793
|
+
}
|
|
11794
|
+
const folder = byUuid.get(uuid);
|
|
11795
|
+
if (!folder) {
|
|
11796
|
+
return [];
|
|
11797
|
+
}
|
|
11798
|
+
if (visited.has(uuid)) {
|
|
11799
|
+
return [];
|
|
11800
|
+
}
|
|
11801
|
+
visited.add(uuid);
|
|
11802
|
+
const path = [...pathFor(folder.parent_uuid, visited), slugify(folder.name)];
|
|
11803
|
+
cache.set(uuid, path);
|
|
11804
|
+
return path;
|
|
11805
|
+
}
|
|
11806
|
+
for (const folder of folders) {
|
|
11807
|
+
pathFor(folder.uuid, /* @__PURE__ */ new Set());
|
|
11808
|
+
}
|
|
11809
|
+
return cache;
|
|
11810
|
+
}
|
|
11811
|
+
|
|
11812
|
+
const FIELD_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "pos"]);
|
|
11813
|
+
function toCamelCaseIdentifier(str) {
|
|
11814
|
+
const camel = slugify(str).replace(/^[_-]+/, "").replace(/[_-]+(.)/g, (_, char) => char.toUpperCase());
|
|
11815
|
+
if (!camel) {
|
|
11816
|
+
return "_";
|
|
11817
|
+
}
|
|
11818
|
+
return /^\d/.test(camel) ? `_${camel}` : camel;
|
|
11819
|
+
}
|
|
11820
|
+
function toKebabCase(str) {
|
|
11821
|
+
return str.replace(/[\s_]+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
11822
|
+
}
|
|
11823
|
+
function componentVarName(name) {
|
|
11824
|
+
return `${toCamelCaseIdentifier(name)}Block`;
|
|
11825
|
+
}
|
|
11826
|
+
function datasourceVarName(name) {
|
|
11827
|
+
return `${toCamelCaseIdentifier(name)}Datasource`;
|
|
11828
|
+
}
|
|
11829
|
+
function resolveVarNames(rawNames, baseVarName) {
|
|
11830
|
+
const used = /* @__PURE__ */ new Set();
|
|
11831
|
+
return rawNames.map((raw) => {
|
|
11832
|
+
const base = baseVarName(raw);
|
|
11833
|
+
let candidate = base;
|
|
11834
|
+
let n = 2;
|
|
11835
|
+
while (used.has(candidate)) {
|
|
11836
|
+
candidate = `${base}${n++}`;
|
|
11837
|
+
}
|
|
11838
|
+
used.add(candidate);
|
|
11839
|
+
return candidate;
|
|
11840
|
+
});
|
|
11841
|
+
}
|
|
11842
|
+
function resolveFileNames(baseNames, dirKeys) {
|
|
11843
|
+
const usedByDir = /* @__PURE__ */ new Map();
|
|
11844
|
+
return baseNames.map((base, i) => {
|
|
11845
|
+
const dir = dirKeys?.[i] ?? "";
|
|
11846
|
+
let used = usedByDir.get(dir);
|
|
11847
|
+
if (!used) {
|
|
11848
|
+
used = /* @__PURE__ */ new Set();
|
|
11849
|
+
usedByDir.set(dir, used);
|
|
11850
|
+
}
|
|
11851
|
+
let candidate = base;
|
|
11852
|
+
let n = 2;
|
|
11853
|
+
while (used.has(candidate)) {
|
|
11854
|
+
candidate = `${base}-${n++}`;
|
|
11855
|
+
}
|
|
11856
|
+
used.add(candidate);
|
|
11857
|
+
return candidate;
|
|
11858
|
+
});
|
|
11859
|
+
}
|
|
11860
|
+
function componentFileName(name) {
|
|
11861
|
+
return toKebabCase(name);
|
|
11862
|
+
}
|
|
11863
|
+
function datasourceFileName(datasource) {
|
|
11864
|
+
return toKebabCase(datasource.slug || datasource.name);
|
|
11865
|
+
}
|
|
11866
|
+
function resolveComponents(components, segmentsByIndex) {
|
|
11867
|
+
const varNames = resolveVarNames(components.map((c) => c.name), componentVarName);
|
|
11868
|
+
const fileNames = resolveFileNames(
|
|
11869
|
+
components.map((c) => componentFileName(c.name)),
|
|
11870
|
+
segmentsByIndex.map((segments) => segments.join("/"))
|
|
11871
|
+
);
|
|
11872
|
+
return components.map((component, i) => ({
|
|
11873
|
+
component,
|
|
11874
|
+
varName: varNames[i],
|
|
11875
|
+
fileName: fileNames[i],
|
|
11876
|
+
segments: segmentsByIndex[i]
|
|
11877
|
+
}));
|
|
11878
|
+
}
|
|
11879
|
+
function resolveDatasources(datasources) {
|
|
11880
|
+
const varNames = resolveVarNames(datasources.map((d) => d.name), datasourceVarName);
|
|
11881
|
+
const fileNames = resolveFileNames(datasources.map((d) => datasourceFileName(d)));
|
|
11882
|
+
return datasources.map((datasource, i) => ({
|
|
11883
|
+
datasource,
|
|
11884
|
+
varName: varNames[i],
|
|
11885
|
+
fileName: fileNames[i]
|
|
11886
|
+
}));
|
|
11887
|
+
}
|
|
11888
|
+
function toDslField(field) {
|
|
11889
|
+
const { component_whitelist, datasource_slug, restrict_components, restrict_type, ...rest } = field;
|
|
11890
|
+
const out = { ...rest };
|
|
11891
|
+
if (component_whitelist !== void 0) {
|
|
11892
|
+
out.allow = component_whitelist;
|
|
11893
|
+
} else {
|
|
11894
|
+
if (restrict_components !== void 0) {
|
|
11895
|
+
out.restrict_components = restrict_components;
|
|
11896
|
+
}
|
|
11897
|
+
if (restrict_type !== void 0) {
|
|
11898
|
+
out.restrict_type = restrict_type;
|
|
11899
|
+
}
|
|
11900
|
+
}
|
|
11901
|
+
if (datasource_slug !== void 0) {
|
|
11902
|
+
out.datasource = datasource_slug;
|
|
11903
|
+
}
|
|
11904
|
+
return out;
|
|
11905
|
+
}
|
|
11906
|
+
function generateFieldCode(fieldName, fieldData, depth) {
|
|
11907
|
+
const clean = toDslField(stripKeys(fieldData, FIELD_STRIP_KEYS));
|
|
11908
|
+
return `defineField(${quoteString(fieldName)}, ${formatValue(clean, depth)})`;
|
|
11909
|
+
}
|
|
11910
|
+
function sortSchemaByPos(schema) {
|
|
11911
|
+
return Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
|
|
11912
|
+
const posA = typeof a.pos === "number" ? a.pos : Infinity;
|
|
11913
|
+
const posB = typeof b.pos === "number" ? b.pos : Infinity;
|
|
11914
|
+
return posA - posB;
|
|
11915
|
+
});
|
|
11916
|
+
}
|
|
11917
|
+
function generateComponentFile(component, varName) {
|
|
11918
|
+
const lines = [];
|
|
11919
|
+
lines.push("import {");
|
|
11920
|
+
lines.push(" defineBlock,");
|
|
11921
|
+
lines.push(" defineField,");
|
|
11922
|
+
lines.push("} from '@storyblok/schema';");
|
|
11923
|
+
lines.push("");
|
|
11924
|
+
const resolvedVarName = varName ?? componentVarName(component.name);
|
|
11925
|
+
lines.push(`export const ${resolvedVarName} = defineBlock({`);
|
|
11926
|
+
const clean = stripKeys(component, COMPONENT_STRIP_KEYS);
|
|
11927
|
+
delete clean.component_group_uuid;
|
|
11928
|
+
const orderedKeys = [];
|
|
11929
|
+
if (clean.name !== void 0) {
|
|
11930
|
+
orderedKeys.push("name");
|
|
11931
|
+
}
|
|
11932
|
+
if (clean.display_name !== void 0) {
|
|
11933
|
+
orderedKeys.push("display_name");
|
|
11934
|
+
}
|
|
11935
|
+
if (clean.is_root !== void 0) {
|
|
11936
|
+
orderedKeys.push("is_root");
|
|
11937
|
+
}
|
|
11938
|
+
if (clean.is_nestable !== void 0) {
|
|
11939
|
+
orderedKeys.push("is_nestable");
|
|
11940
|
+
}
|
|
11941
|
+
const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
|
|
11942
|
+
for (const key of Object.keys(clean).sort()) {
|
|
11943
|
+
if (!handled.has(key)) {
|
|
11944
|
+
orderedKeys.push(key);
|
|
11945
|
+
}
|
|
11946
|
+
}
|
|
11947
|
+
for (const key of orderedKeys) {
|
|
11948
|
+
lines.push(`${INDENT}${key}: ${formatValue(clean[key], 1)},`);
|
|
11949
|
+
}
|
|
11950
|
+
if (clean.schema && typeof clean.schema === "object") {
|
|
11951
|
+
const schema = clean.schema;
|
|
11952
|
+
const sortedFields = sortSchemaByPos(schema);
|
|
11953
|
+
if (sortedFields.length > 0) {
|
|
11954
|
+
lines.push(`${INDENT}fields: [`);
|
|
11955
|
+
for (const [fieldName, fieldData] of sortedFields) {
|
|
11956
|
+
const fieldCode = generateFieldCode(fieldName, fieldData, 2);
|
|
11957
|
+
lines.push(`${INDENT}${INDENT}${fieldCode},`);
|
|
11958
|
+
}
|
|
11959
|
+
lines.push(`${INDENT}],`);
|
|
11960
|
+
} else {
|
|
11961
|
+
lines.push(`${INDENT}fields: [],`);
|
|
11962
|
+
}
|
|
11963
|
+
}
|
|
11964
|
+
lines.push("});");
|
|
11965
|
+
lines.push("");
|
|
11966
|
+
return lines.join("\n");
|
|
11967
|
+
}
|
|
11968
|
+
function generateDatasourceFile(datasource, varName) {
|
|
11969
|
+
const lines = [];
|
|
11970
|
+
lines.push("import { defineDatasource } from '@storyblok/schema';");
|
|
11971
|
+
lines.push("");
|
|
11972
|
+
const resolvedVarName = varName ?? datasourceVarName(datasource.name);
|
|
11973
|
+
lines.push(`export const ${resolvedVarName} = defineDatasource({`);
|
|
11974
|
+
const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
|
|
11975
|
+
if (clean.name !== void 0) {
|
|
11976
|
+
lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
|
|
11977
|
+
}
|
|
11978
|
+
if (clean.slug !== void 0) {
|
|
11979
|
+
lines.push(`${INDENT}slug: ${formatValue(clean.slug, 1)},`);
|
|
11980
|
+
}
|
|
11981
|
+
const handled = /* @__PURE__ */ new Set(["name", "slug"]);
|
|
11982
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
11983
|
+
if (!handled.has(key)) {
|
|
11984
|
+
lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
|
|
11985
|
+
}
|
|
11986
|
+
}
|
|
11987
|
+
lines.push("});");
|
|
11988
|
+
lines.push("");
|
|
11989
|
+
return lines.join("\n");
|
|
11990
|
+
}
|
|
11991
|
+
function generateSchemaFile(components, datasources) {
|
|
11992
|
+
const lines = [];
|
|
11993
|
+
lines.push("import { defineSchema } from '@storyblok/schema';");
|
|
11994
|
+
lines.push("import type { Schema as InferSchema, Story as InferStory } from '@storyblok/schema';");
|
|
11995
|
+
lines.push("import type { MapiStory as InferStoryMapi } from '@storyblok/schema';");
|
|
11996
|
+
lines.push("");
|
|
11997
|
+
for (const { varName, fileName, segments } of components) {
|
|
11998
|
+
const subPath = segments.length > 0 ? `${segments.join("/")}/` : "";
|
|
11999
|
+
lines.push(`import { ${varName} } from './blocks/${subPath}${fileName}';`);
|
|
12000
|
+
}
|
|
12001
|
+
for (const { varName, fileName } of datasources) {
|
|
12002
|
+
lines.push(`import { ${varName} } from './datasources/${fileName}';`);
|
|
12003
|
+
}
|
|
12004
|
+
lines.push("");
|
|
12005
|
+
lines.push("export const schema = defineSchema({");
|
|
12006
|
+
if (components.length > 0) {
|
|
12007
|
+
lines.push(" blocks: {");
|
|
12008
|
+
for (const { varName } of components) {
|
|
12009
|
+
lines.push(` ${varName},`);
|
|
12010
|
+
}
|
|
12011
|
+
lines.push(" },");
|
|
12012
|
+
}
|
|
12013
|
+
if (datasources.length > 0) {
|
|
12014
|
+
lines.push(" datasources: {");
|
|
12015
|
+
for (const { varName } of datasources) {
|
|
12016
|
+
lines.push(` ${varName},`);
|
|
12017
|
+
}
|
|
12018
|
+
lines.push(" },");
|
|
12019
|
+
}
|
|
12020
|
+
lines.push("});");
|
|
12021
|
+
lines.push("");
|
|
12022
|
+
lines.push("export type Schema = InferSchema<typeof schema>;");
|
|
12023
|
+
lines.push("export type Blocks = Schema['blocks'];");
|
|
12024
|
+
lines.push("export type Story = InferStory<Blocks>;");
|
|
12025
|
+
lines.push("export type StoryMapi = InferStoryMapi<Blocks>;");
|
|
12026
|
+
lines.push("");
|
|
12027
|
+
return lines.join("\n");
|
|
12028
|
+
}
|
|
12029
|
+
|
|
12030
|
+
async function writeFileWithDirs(filePath, content) {
|
|
12031
|
+
const dir = dirname(filePath);
|
|
12032
|
+
await mkdir(dir, { recursive: true });
|
|
12033
|
+
await writeFile(filePath, content, "utf-8");
|
|
12034
|
+
}
|
|
12035
|
+
async function writeSchemaFiles(targetPath, components, componentFolders, datasources) {
|
|
12036
|
+
const writtenFiles = [];
|
|
12037
|
+
const groupPathByUuid = buildGroupPathByUuid(componentFolders);
|
|
12038
|
+
const componentSegments = components.map(
|
|
12039
|
+
(comp) => comp.component_group_uuid ? groupPathByUuid.get(comp.component_group_uuid) ?? [] : []
|
|
12040
|
+
);
|
|
12041
|
+
const resolvedComponents = resolveComponents(components, componentSegments);
|
|
12042
|
+
const resolvedDatasources = resolveDatasources(datasources);
|
|
12043
|
+
for (const { component, varName, fileName, segments } of resolvedComponents) {
|
|
12044
|
+
const filePath = join(targetPath, "blocks", ...segments, `${fileName}.ts`);
|
|
12045
|
+
await writeFileWithDirs(filePath, generateComponentFile(component, varName));
|
|
12046
|
+
writtenFiles.push(filePath);
|
|
12047
|
+
}
|
|
12048
|
+
for (const { datasource, varName, fileName } of resolvedDatasources) {
|
|
12049
|
+
const filePath = join(targetPath, "datasources", `${fileName}.ts`);
|
|
12050
|
+
await writeFileWithDirs(filePath, generateDatasourceFile(datasource, varName));
|
|
12051
|
+
writtenFiles.push(filePath);
|
|
12052
|
+
}
|
|
12053
|
+
const schemaPath = join(targetPath, "schema.ts");
|
|
12054
|
+
await writeFileWithDirs(schemaPath, generateSchemaFile(resolvedComponents, resolvedDatasources));
|
|
12055
|
+
writtenFiles.push(schemaPath);
|
|
12056
|
+
return writtenFiles;
|
|
12057
|
+
}
|
|
12058
|
+
|
|
12059
|
+
async function isTargetEmpty(targetPath) {
|
|
12060
|
+
try {
|
|
12061
|
+
const entries = await readdir(targetPath);
|
|
12062
|
+
return entries.every((entry) => entry.startsWith("."));
|
|
12063
|
+
} catch (maybeError) {
|
|
12064
|
+
const error = maybeError;
|
|
12065
|
+
if (error?.code === "ENOENT") {
|
|
12066
|
+
return true;
|
|
12067
|
+
}
|
|
12068
|
+
throw error;
|
|
12069
|
+
}
|
|
12070
|
+
}
|
|
12071
|
+
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) => {
|
|
12072
|
+
const ui = getUI();
|
|
12073
|
+
const logger = getLogger();
|
|
12074
|
+
const reporter = getReporter();
|
|
12075
|
+
const { space, verbose } = command.optsWithGlobals();
|
|
12076
|
+
const { state } = session();
|
|
12077
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Initializing schema...");
|
|
12078
|
+
logger.info("Schema init started", { space });
|
|
12079
|
+
if (!requireAuthentication(state, verbose)) {
|
|
12080
|
+
return;
|
|
12081
|
+
}
|
|
12082
|
+
if (!space) {
|
|
12083
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
12084
|
+
return;
|
|
12085
|
+
}
|
|
12086
|
+
const targetPath = resolve(options.outDir);
|
|
12087
|
+
const targetDisplayPath = displayPath(targetPath, options.outDir);
|
|
12088
|
+
if (!await isTargetEmpty(targetPath)) {
|
|
12089
|
+
handleError(
|
|
12090
|
+
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.`),
|
|
12091
|
+
verbose
|
|
12092
|
+
);
|
|
12093
|
+
return;
|
|
12094
|
+
}
|
|
12095
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
12096
|
+
try {
|
|
12097
|
+
const fetchSpinner = ui.createSpinner(`Fetching schema from space ${space}...`);
|
|
12098
|
+
let fetchResult;
|
|
12099
|
+
try {
|
|
12100
|
+
fetchResult = await fetchRemoteSchema(space);
|
|
12101
|
+
} catch (maybeError) {
|
|
12102
|
+
fetchSpinner.failed("Failed to fetch remote schema");
|
|
12103
|
+
handleError(toError(maybeError), verbose);
|
|
12104
|
+
return;
|
|
12105
|
+
}
|
|
12106
|
+
const { rawComponents, rawComponentFolders, rawDatasources } = fetchResult;
|
|
12107
|
+
fetchSpinner.succeed(`Found: ${rawComponents.length} components, ${rawComponentFolders.length} component folders, ${rawDatasources.length} datasources`);
|
|
12108
|
+
const writeSpinner = ui.createSpinner(`Generating TypeScript files to ${targetDisplayPath}...`);
|
|
12109
|
+
const writtenFiles = await writeSchemaFiles(targetPath, rawComponents, rawComponentFolders, rawDatasources);
|
|
12110
|
+
summary.total = writtenFiles.length;
|
|
12111
|
+
summary.succeeded = writtenFiles.length;
|
|
12112
|
+
writeSpinner.succeed(`Generated ${writtenFiles.length} files`);
|
|
12113
|
+
ui.list(writtenFiles.map((file) => displayPath(file, options.outDir)));
|
|
12114
|
+
ui.warn("`schema init` is a one-time bootstrap step for adopting an existing space. Review generated files before continuing.");
|
|
12115
|
+
ui.info("After bootstrapping, keep your local schema as the source of truth and use `schema push` for ongoing changes.");
|
|
12116
|
+
ui.info("Make sure `@storyblok/schema` is installed in the project that imports these files (e.g. `pnpm add @storyblok/schema`).");
|
|
12117
|
+
} catch (maybeError) {
|
|
12118
|
+
summary.failed += 1;
|
|
12119
|
+
handleError(toError(maybeError), verbose);
|
|
12120
|
+
} finally {
|
|
12121
|
+
logger.info("Schema init finished", { summary });
|
|
12122
|
+
reporter.addSummary("schemaInitResults", summary);
|
|
12123
|
+
reporter.finalize();
|
|
12124
|
+
}
|
|
12125
|
+
});
|
|
12126
|
+
|
|
12127
|
+
const API_ASSIGNED_FIELDS = [
|
|
12128
|
+
"id",
|
|
12129
|
+
"created_at",
|
|
12130
|
+
"updated_at",
|
|
12131
|
+
"real_name",
|
|
12132
|
+
"all_presets",
|
|
12133
|
+
"image",
|
|
12134
|
+
"uuid"
|
|
12135
|
+
];
|
|
12136
|
+
function stripApiFields(payload) {
|
|
12137
|
+
const result = { ...payload };
|
|
12138
|
+
for (const field of API_ASSIGNED_FIELDS) {
|
|
12139
|
+
delete result[field];
|
|
12140
|
+
}
|
|
12141
|
+
return result;
|
|
12142
|
+
}
|
|
12143
|
+
async function listChangesets(basePath) {
|
|
12144
|
+
const dir = join(basePath, "schema", "changesets");
|
|
12145
|
+
if (!await fileExists(dir)) {
|
|
12146
|
+
return [];
|
|
12147
|
+
}
|
|
12148
|
+
const files = await readDirectory(dir);
|
|
12149
|
+
return files.filter((f) => f.endsWith(".json")).sort().reverse().map((f) => join(dir, f));
|
|
12150
|
+
}
|
|
12151
|
+
async function loadChangeset(filePath) {
|
|
12152
|
+
const content = await readFile$1(filePath, "utf-8");
|
|
12153
|
+
return JSON.parse(content);
|
|
12154
|
+
}
|
|
12155
|
+
function buildRollbackOps(changeset) {
|
|
12156
|
+
if (changeset.changes.length === 0) {
|
|
12157
|
+
return [];
|
|
12158
|
+
}
|
|
12159
|
+
return changeset.changes.map((entry) => {
|
|
12160
|
+
switch (entry.action) {
|
|
12161
|
+
case "create":
|
|
12162
|
+
return { type: entry.type, name: entry.name, action: "delete", payload: {} };
|
|
12163
|
+
case "update":
|
|
12164
|
+
return { type: entry.type, name: entry.name, action: "update", payload: entry.before ?? {} };
|
|
12165
|
+
case "delete":
|
|
12166
|
+
return { type: entry.type, name: entry.name, action: "create", payload: entry.before ?? {} };
|
|
12167
|
+
default:
|
|
12168
|
+
return { type: entry.type, name: entry.name, action: entry.action, payload: {} };
|
|
12169
|
+
}
|
|
12170
|
+
});
|
|
12171
|
+
}
|
|
12172
|
+
function rollbackAction(original) {
|
|
12173
|
+
switch (original) {
|
|
12174
|
+
case "create":
|
|
12175
|
+
return "delete";
|
|
12176
|
+
case "update":
|
|
12177
|
+
return "update";
|
|
12178
|
+
case "delete":
|
|
12179
|
+
return "create";
|
|
12180
|
+
}
|
|
12181
|
+
}
|
|
12182
|
+
function formatRollbackOutput(changes) {
|
|
12183
|
+
const byType = {
|
|
12184
|
+
component: [],
|
|
12185
|
+
datasource: []
|
|
12186
|
+
};
|
|
12187
|
+
for (const entry of changes) {
|
|
12188
|
+
byType[entry.type]?.push(entry);
|
|
12189
|
+
}
|
|
12190
|
+
const icons = {
|
|
12191
|
+
create: chalk.green("+"),
|
|
12192
|
+
update: chalk.yellow("~"),
|
|
12193
|
+
delete: chalk.red("-")
|
|
12194
|
+
};
|
|
12195
|
+
const lines = [];
|
|
12196
|
+
const sections = [
|
|
12197
|
+
["Components", byType.component],
|
|
12198
|
+
["Datasources", byType.datasource]
|
|
12199
|
+
];
|
|
12200
|
+
for (const [label, entries] of sections) {
|
|
12201
|
+
if (entries.length === 0) {
|
|
12202
|
+
continue;
|
|
12203
|
+
}
|
|
12204
|
+
lines.push(chalk.bold(label));
|
|
12205
|
+
for (const entry of entries) {
|
|
12206
|
+
const action = rollbackAction(entry.action);
|
|
12207
|
+
const icon = icons[action] ?? " ";
|
|
12208
|
+
const name = action === "delete" ? chalk.red(entry.name) : entry.name;
|
|
12209
|
+
lines.push(` ${icon} ${name} ${chalk.dim(`(${action})`)}`);
|
|
12210
|
+
if (entry.action === "update" && entry.before && entry.after) {
|
|
12211
|
+
let fromStr;
|
|
12212
|
+
let toStr;
|
|
12213
|
+
if (entry.type === "component") {
|
|
12214
|
+
fromStr = serializeComponent(applyDefaults(entry.after, COMPONENT_DEFAULTS));
|
|
12215
|
+
toStr = serializeComponent(applyDefaults(entry.before, COMPONENT_DEFAULTS));
|
|
12216
|
+
} else {
|
|
12217
|
+
fromStr = serializeDatasource(entry.after);
|
|
12218
|
+
toStr = serializeDatasource(entry.before);
|
|
12219
|
+
}
|
|
12220
|
+
if (fromStr !== toStr) {
|
|
12221
|
+
const patch = createTwoFilesPatch(
|
|
12222
|
+
`current/${entry.name}`,
|
|
12223
|
+
`restore/${entry.name}`,
|
|
12224
|
+
fromStr,
|
|
12225
|
+
toStr,
|
|
12226
|
+
"current",
|
|
12227
|
+
"restore"
|
|
12228
|
+
);
|
|
12229
|
+
for (const line of patch.split("\n")) {
|
|
12230
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
12231
|
+
lines.push(` ${chalk.green(line)}`);
|
|
12232
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
12233
|
+
lines.push(` ${chalk.red(line)}`);
|
|
12234
|
+
}
|
|
12235
|
+
}
|
|
12236
|
+
}
|
|
12237
|
+
}
|
|
12238
|
+
}
|
|
12239
|
+
lines.push("");
|
|
12240
|
+
}
|
|
12241
|
+
return lines.join("\n").trimEnd();
|
|
12242
|
+
}
|
|
12243
|
+
async function executeRollback(spaceId, ops, remote) {
|
|
12244
|
+
const client = getMapiClient();
|
|
12245
|
+
const spaceIdNum = Number(spaceId);
|
|
12246
|
+
let created = 0;
|
|
12247
|
+
let updated = 0;
|
|
12248
|
+
let deleted = 0;
|
|
12249
|
+
const componentOps = ops.filter((op) => op.type === "component");
|
|
12250
|
+
const datasourceOps = ops.filter((op) => op.type === "datasource");
|
|
12251
|
+
for (const op of componentOps.filter((o) => o.action !== "delete")) {
|
|
12252
|
+
if (op.action === "create") {
|
|
12253
|
+
const payload = toComponentCreate(stripApiFields(op.payload));
|
|
12254
|
+
try {
|
|
12255
|
+
await client.components.create({
|
|
12256
|
+
path: { space_id: spaceIdNum },
|
|
12257
|
+
body: { component: payload },
|
|
12258
|
+
throwOnError: true
|
|
12259
|
+
});
|
|
12260
|
+
created++;
|
|
12261
|
+
} catch (error) {
|
|
12262
|
+
handleAPIError("push_component", error, `Failed to create component ${op.name}`);
|
|
12263
|
+
}
|
|
12264
|
+
} else if (op.action === "update") {
|
|
12265
|
+
const existing = remote.components.get(op.name);
|
|
12266
|
+
if (existing?.id) {
|
|
12267
|
+
const payload = toComponentUpdate(stripApiFields(op.payload));
|
|
12268
|
+
try {
|
|
12269
|
+
await client.components.update(existing.id, {
|
|
12270
|
+
path: { space_id: spaceIdNum },
|
|
12271
|
+
body: { component: payload },
|
|
12272
|
+
throwOnError: true
|
|
12273
|
+
});
|
|
12274
|
+
updated++;
|
|
12275
|
+
} catch (error) {
|
|
12276
|
+
handleAPIError("update_component", error, `Failed to update component ${op.name}`);
|
|
12277
|
+
}
|
|
12278
|
+
}
|
|
12279
|
+
}
|
|
12280
|
+
}
|
|
12281
|
+
for (const op of datasourceOps.filter((o) => o.action !== "delete")) {
|
|
12282
|
+
if (op.action === "create") {
|
|
12283
|
+
const payload = toDatasourceCreate(stripApiFields(op.payload));
|
|
12284
|
+
try {
|
|
12285
|
+
await client.datasources.create({
|
|
12286
|
+
path: { space_id: spaceIdNum },
|
|
12287
|
+
body: { datasource: payload },
|
|
12288
|
+
throwOnError: true
|
|
12289
|
+
});
|
|
12290
|
+
created++;
|
|
12291
|
+
} catch (error) {
|
|
12292
|
+
handleAPIError("push_datasource", error, `Failed to create datasource ${op.name}`);
|
|
12293
|
+
}
|
|
12294
|
+
} else if (op.action === "update") {
|
|
12295
|
+
const existing = remote.datasources.get(op.name);
|
|
12296
|
+
if (existing?.id) {
|
|
12297
|
+
const payload = toDatasourceUpdate(stripApiFields(op.payload), existing);
|
|
12298
|
+
try {
|
|
12299
|
+
await client.datasources.update(existing.id, {
|
|
12300
|
+
path: { space_id: spaceIdNum },
|
|
12301
|
+
body: { datasource: payload },
|
|
12302
|
+
throwOnError: true
|
|
12303
|
+
});
|
|
12304
|
+
updated++;
|
|
12305
|
+
} catch (error) {
|
|
12306
|
+
handleAPIError("update_datasource", error, `Failed to update datasource ${op.name}`);
|
|
12307
|
+
}
|
|
12308
|
+
}
|
|
12309
|
+
}
|
|
12310
|
+
}
|
|
12311
|
+
for (const op of datasourceOps.filter((o) => o.action === "delete")) {
|
|
12312
|
+
const existing = remote.datasources.get(op.name);
|
|
12313
|
+
if (existing?.id) {
|
|
12314
|
+
try {
|
|
12315
|
+
await client.datasources.delete(existing.id, {
|
|
12316
|
+
path: { space_id: spaceIdNum },
|
|
12317
|
+
throwOnError: true
|
|
12318
|
+
});
|
|
12319
|
+
deleted++;
|
|
12320
|
+
} catch (error) {
|
|
12321
|
+
handleAPIError("delete_datasource", error, `Failed to delete datasource ${op.name}`);
|
|
12322
|
+
}
|
|
12323
|
+
}
|
|
12324
|
+
}
|
|
12325
|
+
for (const op of componentOps.filter((o) => o.action === "delete")) {
|
|
12326
|
+
const existing = remote.components.get(op.name);
|
|
12327
|
+
if (existing?.id) {
|
|
12328
|
+
try {
|
|
12329
|
+
await client.components.delete(existing.id, {
|
|
12330
|
+
path: { space_id: spaceIdNum },
|
|
12331
|
+
throwOnError: true
|
|
12332
|
+
});
|
|
12333
|
+
deleted++;
|
|
12334
|
+
} catch (error) {
|
|
12335
|
+
handleAPIError("delete_component", error, `Failed to delete component ${op.name}`);
|
|
12336
|
+
}
|
|
12337
|
+
}
|
|
12338
|
+
}
|
|
12339
|
+
return { created, updated, deleted };
|
|
12340
|
+
}
|
|
12341
|
+
|
|
12342
|
+
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) => {
|
|
12343
|
+
const ui = getUI();
|
|
12344
|
+
const logger = getLogger();
|
|
12345
|
+
const reporter = getReporter();
|
|
12346
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
12347
|
+
const { state } = session();
|
|
12348
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Rolling back schema...");
|
|
12349
|
+
logger.info("Schema rollback started", { changesetFile, space });
|
|
12350
|
+
if (!requireAuthentication(state, verbose)) {
|
|
12351
|
+
return;
|
|
12352
|
+
}
|
|
12353
|
+
if (!space) {
|
|
12354
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
12355
|
+
return;
|
|
12356
|
+
}
|
|
12357
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
12358
|
+
try {
|
|
12359
|
+
const resolvedBase = resolvePath(basePath, "");
|
|
12360
|
+
let resolvedFile;
|
|
12361
|
+
if (changesetFile) {
|
|
12362
|
+
resolvedFile = changesetFile;
|
|
12363
|
+
} else if (options.latest) {
|
|
12364
|
+
const available = await listChangesets(resolvedBase);
|
|
12365
|
+
if (available.length === 0) {
|
|
12366
|
+
ui.warn("No changesets found. Run `schema push` first to create one.");
|
|
12367
|
+
return;
|
|
12368
|
+
}
|
|
12369
|
+
resolvedFile = available[0];
|
|
12370
|
+
ui.info(`Using latest changeset: ${basename(resolvedFile)}`);
|
|
12371
|
+
} else {
|
|
12372
|
+
const available = await listChangesets(resolvedBase);
|
|
12373
|
+
if (available.length === 0) {
|
|
12374
|
+
ui.warn("No changesets found. Run `schema push` first to create one.");
|
|
12375
|
+
return;
|
|
12376
|
+
}
|
|
12377
|
+
resolvedFile = await select({
|
|
12378
|
+
message: "Select a changeset to roll back:",
|
|
12379
|
+
choices: available.map((f) => ({ name: basename(f), value: f }))
|
|
12380
|
+
});
|
|
12381
|
+
}
|
|
12382
|
+
let changeset;
|
|
12383
|
+
try {
|
|
12384
|
+
changeset = await loadChangeset(resolvedFile);
|
|
12385
|
+
} catch (maybeError) {
|
|
12386
|
+
handleError(toError(maybeError), verbose);
|
|
12387
|
+
return;
|
|
12388
|
+
}
|
|
12389
|
+
logger.info("Changeset loaded", { file: resolvedFile, changes: changeset.changes.length });
|
|
12390
|
+
const ops = buildRollbackOps(changeset);
|
|
12391
|
+
if (ops.length === 0) {
|
|
12392
|
+
ui.ok("Changeset has no changes \u2014 nothing to roll back.");
|
|
12393
|
+
return;
|
|
12394
|
+
}
|
|
12395
|
+
ui.br();
|
|
12396
|
+
ui.log(formatRollbackOutput(changeset.changes));
|
|
12397
|
+
if (options.dryRun) {
|
|
12398
|
+
ui.info("Dry run \u2014 no changes applied.");
|
|
12399
|
+
logger.info("Dry run completed", { ops: ops.length });
|
|
12400
|
+
return;
|
|
12401
|
+
}
|
|
12402
|
+
if (!options.yes) {
|
|
12403
|
+
const confirmed = await confirm({
|
|
12404
|
+
message: `Apply rollback of ${ops.length} change(s) from ${basename(resolvedFile)}?`,
|
|
12405
|
+
default: false
|
|
12406
|
+
});
|
|
12407
|
+
if (!confirmed) {
|
|
12408
|
+
ui.info("Rollback cancelled.");
|
|
12409
|
+
return;
|
|
12410
|
+
}
|
|
12411
|
+
}
|
|
12412
|
+
const remoteSpinner = ui.createSpinner(`Fetching current remote state from space ${space}...`);
|
|
12413
|
+
let remoteResult;
|
|
12414
|
+
try {
|
|
12415
|
+
remoteResult = await fetchRemoteSchema(space);
|
|
12416
|
+
} catch (maybeError) {
|
|
12417
|
+
remoteSpinner.failed("Failed to fetch remote schema");
|
|
12418
|
+
handleError(toError(maybeError), verbose);
|
|
12419
|
+
return;
|
|
12420
|
+
}
|
|
12421
|
+
const { remote } = remoteResult;
|
|
12422
|
+
remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
|
|
12423
|
+
const rollbackSpinner = ui.createSpinner("Applying rollback...");
|
|
12424
|
+
let result;
|
|
12425
|
+
try {
|
|
12426
|
+
result = await executeRollback(space, ops, remote);
|
|
12427
|
+
} catch (error) {
|
|
12428
|
+
rollbackSpinner.failed("Failed to apply rollback");
|
|
12429
|
+
throw error;
|
|
12430
|
+
}
|
|
12431
|
+
summary.total = result.created + result.updated + result.deleted;
|
|
12432
|
+
summary.succeeded = summary.total;
|
|
12433
|
+
rollbackSpinner.succeed(`Rolled back: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted.`);
|
|
12434
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
12435
|
+
const rollbackChangesetPath = await saveChangeset(resolvedBase, {
|
|
12436
|
+
timestamp,
|
|
12437
|
+
spaceId: Number(space),
|
|
12438
|
+
remote: { components: remoteResult.rawComponents, componentFolders: remoteResult.rawComponentFolders, datasources: remoteResult.rawDatasources },
|
|
12439
|
+
changes: ops.map((op) => ({
|
|
12440
|
+
type: op.type,
|
|
12441
|
+
name: op.name,
|
|
12442
|
+
action: op.action,
|
|
12443
|
+
...Object.keys(op.payload).length > 0 && { after: op.payload }
|
|
12444
|
+
}))
|
|
12445
|
+
});
|
|
12446
|
+
logger.info("Rollback changeset saved", { path: displayPath(rollbackChangesetPath, basePath) });
|
|
12447
|
+
} catch (maybeError) {
|
|
12448
|
+
summary.failed += 1;
|
|
12449
|
+
handleError(toError(maybeError), verbose);
|
|
12450
|
+
} finally {
|
|
12451
|
+
logger.info("Schema rollback finished", { summary });
|
|
12452
|
+
reporter.addSummary("schemaRollbackResults", summary);
|
|
12453
|
+
reporter.finalize();
|
|
12454
|
+
}
|
|
12455
|
+
});
|
|
12456
|
+
|
|
10545
12457
|
const program = getProgram();
|
|
10546
12458
|
konsola.br();
|
|
10547
12459
|
konsola.br();
|