storyblok 4.19.2 → 4.20.0-alpha.1
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 +2596 -155
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -4
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",
|
|
@@ -695,6 +698,8 @@ const API_ACTIONS = {
|
|
|
695
698
|
pull_component_internal_tags: "Failed to pull component internal tags",
|
|
696
699
|
push_component: "Failed to push component",
|
|
697
700
|
push_component_group: "Failed to push component group",
|
|
701
|
+
push_component_folder: "Failed to create component folder",
|
|
702
|
+
delete_component_folder: "Failed to delete component folder",
|
|
698
703
|
push_component_preset: "Failed to push component preset",
|
|
699
704
|
push_component_internal_tag: "Failed to push component internal tag",
|
|
700
705
|
update_component: "Failed to update component",
|
|
@@ -702,6 +707,7 @@ const API_ACTIONS = {
|
|
|
702
707
|
update_component_group: "Failed to update component group",
|
|
703
708
|
update_component_preset: "Failed to update component preset",
|
|
704
709
|
delete_component_preset: "Failed to delete component preset",
|
|
710
|
+
delete_component: "Failed to delete component",
|
|
705
711
|
pull_stories: "Failed to pull stories",
|
|
706
712
|
pull_story: "Failed to pull story",
|
|
707
713
|
create_story: "Failed to create story",
|
|
@@ -729,6 +735,8 @@ const API_ACTIONS = {
|
|
|
729
735
|
push_datasource: "Failed to push datasource",
|
|
730
736
|
update_datasource: "Failed to update datasource",
|
|
731
737
|
delete_datasource: "Failed to delete datasource",
|
|
738
|
+
push_datasource_entry: "Failed to push datasource entry",
|
|
739
|
+
update_datasource_entry: "Failed to update datasource entry",
|
|
732
740
|
delete_datasource_entry: "Failed to delete datasource entry",
|
|
733
741
|
create_space: "Failed to create space",
|
|
734
742
|
pull_spaces: "Failed to pull spaces",
|
|
@@ -800,7 +808,11 @@ class APIError extends Error {
|
|
|
800
808
|
if (this.code === 422) {
|
|
801
809
|
const responseData = this.response?.data;
|
|
802
810
|
if (responseData?.name?.[0] === "has already been taken") {
|
|
803
|
-
|
|
811
|
+
if (action === "push_component_folder") {
|
|
812
|
+
this.message = "A component folder with this name already exists";
|
|
813
|
+
} else if (action === "push_component" || action === "update_component") {
|
|
814
|
+
this.message = "A component with this name already exists";
|
|
815
|
+
}
|
|
804
816
|
}
|
|
805
817
|
Object.entries(responseData || {}).forEach(([key, errors]) => {
|
|
806
818
|
if (Array.isArray(errors)) {
|
|
@@ -1078,6 +1090,7 @@ function maskToken(token) {
|
|
|
1078
1090
|
const maskedPart = "*".repeat(token.length - 4);
|
|
1079
1091
|
return `${visiblePart}${maskedPart}`;
|
|
1080
1092
|
}
|
|
1093
|
+
const slugify = (text) => text.toString().toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]+/g, "").replace(/-{2,}/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
1081
1094
|
function createRegexFromGlob(pattern) {
|
|
1082
1095
|
return new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*")}$`);
|
|
1083
1096
|
}
|
|
@@ -1159,6 +1172,25 @@ function getPackageJson() {
|
|
|
1159
1172
|
return packageJson$1;
|
|
1160
1173
|
}
|
|
1161
1174
|
|
|
1175
|
+
async function fetchAllPages(fetchFunction, extractDataFunction) {
|
|
1176
|
+
const items = [];
|
|
1177
|
+
let page = 1;
|
|
1178
|
+
while (true) {
|
|
1179
|
+
const { data, response } = await fetchFunction(page);
|
|
1180
|
+
const totalHeader = response.headers.get("total");
|
|
1181
|
+
const fetchedItems = extractDataFunction(data);
|
|
1182
|
+
items.push(...fetchedItems);
|
|
1183
|
+
if (!totalHeader) {
|
|
1184
|
+
return items;
|
|
1185
|
+
}
|
|
1186
|
+
const total = Number(totalHeader);
|
|
1187
|
+
if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
|
|
1188
|
+
return items;
|
|
1189
|
+
}
|
|
1190
|
+
page++;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1162
1194
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
1163
1195
|
const __dirname$1 = dirname(__filename$1);
|
|
1164
1196
|
function isRegion(value) {
|
|
@@ -1247,6 +1279,10 @@ class UI {
|
|
|
1247
1279
|
this.br();
|
|
1248
1280
|
}
|
|
1249
1281
|
}
|
|
1282
|
+
/** Plain console.log passthrough — use for preformatted or multi-line text. */
|
|
1283
|
+
log(message) {
|
|
1284
|
+
this.console?.log(message);
|
|
1285
|
+
}
|
|
1250
1286
|
list(items) {
|
|
1251
1287
|
for (const item of items) {
|
|
1252
1288
|
this.console?.log(` ${item}`);
|
|
@@ -2164,14 +2200,14 @@ async function performInteractiveLogin(options) {
|
|
|
2164
2200
|
}
|
|
2165
2201
|
}
|
|
2166
2202
|
|
|
2167
|
-
const program$
|
|
2203
|
+
const program$f = getProgram();
|
|
2168
2204
|
const allRegionsText = Object.values(regions).join(",");
|
|
2169
|
-
program$
|
|
2205
|
+
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
2206
|
"-r, --region <region>",
|
|
2171
2207
|
`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
2208
|
).action(async (options) => {
|
|
2173
2209
|
konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
|
|
2174
|
-
const verbose = program$
|
|
2210
|
+
const verbose = program$f.opts().verbose;
|
|
2175
2211
|
const { token, region } = options;
|
|
2176
2212
|
const { state, updateSession, persistCredentials } = session();
|
|
2177
2213
|
if (state.isLoggedIn && !state.envLogin) {
|
|
@@ -2229,10 +2265,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
|
|
|
2229
2265
|
konsola.br();
|
|
2230
2266
|
});
|
|
2231
2267
|
|
|
2232
|
-
const program$
|
|
2233
|
-
program$
|
|
2268
|
+
const program$e = getProgram();
|
|
2269
|
+
program$e.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
|
|
2234
2270
|
konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
|
|
2235
|
-
const verbose = program$
|
|
2271
|
+
const verbose = program$e.opts().verbose;
|
|
2236
2272
|
try {
|
|
2237
2273
|
const { state } = session();
|
|
2238
2274
|
if (!state.isLoggedIn || !state.password || !state.region) {
|
|
@@ -2279,10 +2315,10 @@ async function openSignupInBrowser(url) {
|
|
|
2279
2315
|
}
|
|
2280
2316
|
}
|
|
2281
2317
|
|
|
2282
|
-
const program$
|
|
2283
|
-
program$
|
|
2318
|
+
const program$d = getProgram();
|
|
2319
|
+
program$d.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
|
|
2284
2320
|
konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
|
|
2285
|
-
const verbose = program$
|
|
2321
|
+
const verbose = program$d.opts().verbose;
|
|
2286
2322
|
const { state } = session();
|
|
2287
2323
|
if (state.isLoggedIn && !state.envLogin) {
|
|
2288
2324
|
konsola.ok(`You are already logged in. If you want to signup with a different account, please logout first.`);
|
|
@@ -2303,10 +2339,10 @@ program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
|
|
|
2303
2339
|
konsola.br();
|
|
2304
2340
|
});
|
|
2305
2341
|
|
|
2306
|
-
const program$
|
|
2307
|
-
program$
|
|
2342
|
+
const program$c = getProgram();
|
|
2343
|
+
program$c.command(commands.USER).description("Get the current user").action(async () => {
|
|
2308
2344
|
konsola.title(`${commands.USER}`, colorPalette.USER);
|
|
2309
|
-
const verbose = program$
|
|
2345
|
+
const verbose = program$c.opts().verbose;
|
|
2310
2346
|
const { state } = session();
|
|
2311
2347
|
if (!requireAuthentication(state)) {
|
|
2312
2348
|
return;
|
|
@@ -2334,10 +2370,10 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
|
|
|
2334
2370
|
konsola.br();
|
|
2335
2371
|
});
|
|
2336
2372
|
|
|
2337
|
-
const program$
|
|
2338
|
-
const componentsCommand = program$
|
|
2373
|
+
const program$b = getProgram();
|
|
2374
|
+
const componentsCommand = program$b.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`);
|
|
2339
2375
|
|
|
2340
|
-
function isComponent(item) {
|
|
2376
|
+
function isComponent$1(item) {
|
|
2341
2377
|
return "schema" in item;
|
|
2342
2378
|
}
|
|
2343
2379
|
function isPreset(item) {
|
|
@@ -2371,7 +2407,7 @@ async function loadComponents(directoryPath, options) {
|
|
|
2371
2407
|
throw new Error('Internal tag is missing "id"!');
|
|
2372
2408
|
}
|
|
2373
2409
|
tagMap.set(item.id, item);
|
|
2374
|
-
} else if (isComponent(item)) {
|
|
2410
|
+
} else if (isComponent$1(item)) {
|
|
2375
2411
|
const existing = componentMap.get(item.name);
|
|
2376
2412
|
if (existing) {
|
|
2377
2413
|
duplicates.push(`Component "${item.name}" found in both "${existing.file}" and "${file}"`);
|
|
@@ -2407,44 +2443,20 @@ To fix this, either:
|
|
|
2407
2443
|
}
|
|
2408
2444
|
|
|
2409
2445
|
const DEFAULT_COMPONENTS_FILENAME = "components";
|
|
2410
|
-
const DEFAULT_GROUPS_FILENAME = "groups";
|
|
2446
|
+
const DEFAULT_GROUPS_FILENAME$1 = "groups";
|
|
2411
2447
|
const DEFAULT_PRESETS_FILENAME = "presets";
|
|
2412
2448
|
const DEFAULT_TAGS_FILENAME = "tags";
|
|
2413
2449
|
|
|
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
2450
|
const fetchComponents = async (spaceId) => {
|
|
2434
2451
|
try {
|
|
2435
2452
|
const client = getMapiClient();
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
},
|
|
2444
|
-
throwOnError: true
|
|
2445
|
-
}),
|
|
2446
|
-
(data) => data?.components ?? []
|
|
2447
|
-
);
|
|
2453
|
+
const { data } = await client.components.list({
|
|
2454
|
+
path: {
|
|
2455
|
+
space_id: Number(spaceId)
|
|
2456
|
+
},
|
|
2457
|
+
throwOnError: true
|
|
2458
|
+
});
|
|
2459
|
+
return data?.components ?? [];
|
|
2448
2460
|
} catch (error) {
|
|
2449
2461
|
handleAPIError("pull_components", error);
|
|
2450
2462
|
}
|
|
@@ -2452,19 +2464,16 @@ const fetchComponents = async (spaceId) => {
|
|
|
2452
2464
|
const fetchComponent = async (spaceId, componentName) => {
|
|
2453
2465
|
try {
|
|
2454
2466
|
const client = getMapiClient();
|
|
2455
|
-
const
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
}),
|
|
2466
|
-
(data) => data?.components ?? []
|
|
2467
|
-
);
|
|
2467
|
+
const { data } = await client.components.list({
|
|
2468
|
+
path: {
|
|
2469
|
+
space_id: Number(spaceId)
|
|
2470
|
+
},
|
|
2471
|
+
query: {
|
|
2472
|
+
search: componentName
|
|
2473
|
+
},
|
|
2474
|
+
throwOnError: true
|
|
2475
|
+
});
|
|
2476
|
+
const matches = data?.components ?? [];
|
|
2468
2477
|
return matches.find((c) => c.name === componentName);
|
|
2469
2478
|
} catch (error) {
|
|
2470
2479
|
handleAPIError("pull_components", error, `Failed to fetch component ${componentName}`);
|
|
@@ -2531,7 +2540,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2531
2540
|
const presetsFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.${suffix}.json` : `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.json`);
|
|
2532
2541
|
await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2));
|
|
2533
2542
|
}
|
|
2534
|
-
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
|
|
2543
|
+
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
|
|
2535
2544
|
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
|
|
2536
2545
|
const internalTagsFilePath = join(resolvedPath, suffix ? `${DEFAULT_TAGS_FILENAME}.${suffix}.json` : `${DEFAULT_TAGS_FILENAME}.json`);
|
|
2537
2546
|
await saveToFile(internalTagsFilePath, JSON.stringify(internalTags, null, 2));
|
|
@@ -2541,7 +2550,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2541
2550
|
const componentsFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
|
|
2542
2551
|
await saveToFile(componentsFilePath, JSON.stringify(components, null, 2));
|
|
2543
2552
|
if (groups.length > 0) {
|
|
2544
|
-
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
|
|
2553
|
+
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
|
|
2545
2554
|
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
|
|
2546
2555
|
}
|
|
2547
2556
|
if (presets.length > 0) {
|
|
@@ -2557,6 +2566,27 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2557
2566
|
}
|
|
2558
2567
|
};
|
|
2559
2568
|
|
|
2569
|
+
function isSchemaField$1(value) {
|
|
2570
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
2571
|
+
}
|
|
2572
|
+
function toWritableSchema(schema) {
|
|
2573
|
+
if (!schema) {
|
|
2574
|
+
return void 0;
|
|
2575
|
+
}
|
|
2576
|
+
const result = {};
|
|
2577
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2578
|
+
if (isSchemaField$1(value)) {
|
|
2579
|
+
result[key] = value;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
return result;
|
|
2583
|
+
}
|
|
2584
|
+
function toRequestTagIds(tagIds) {
|
|
2585
|
+
if (!tagIds) {
|
|
2586
|
+
return void 0;
|
|
2587
|
+
}
|
|
2588
|
+
return tagIds.map((id) => Number(id));
|
|
2589
|
+
}
|
|
2560
2590
|
const pushComponent = async (space, component) => {
|
|
2561
2591
|
try {
|
|
2562
2592
|
const client = getMapiClient();
|
|
@@ -2591,10 +2621,23 @@ const updateComponent = async (space, componentId, component) => {
|
|
|
2591
2621
|
}
|
|
2592
2622
|
};
|
|
2593
2623
|
const upsertComponent = async (space, component, existingId) => {
|
|
2624
|
+
const { name, display_name, schema, is_root, is_nestable, component_group_uuid, color, icon, preview_field, internal_tag_ids } = component;
|
|
2625
|
+
const payload = {
|
|
2626
|
+
name,
|
|
2627
|
+
display_name: display_name ?? void 0,
|
|
2628
|
+
schema: toWritableSchema(schema),
|
|
2629
|
+
is_root,
|
|
2630
|
+
is_nestable,
|
|
2631
|
+
component_group_uuid: component_group_uuid ?? void 0,
|
|
2632
|
+
color: color ?? void 0,
|
|
2633
|
+
icon: icon ?? void 0,
|
|
2634
|
+
preview_field: preview_field ?? void 0,
|
|
2635
|
+
internal_tag_ids: toRequestTagIds(internal_tag_ids)
|
|
2636
|
+
};
|
|
2594
2637
|
if (existingId) {
|
|
2595
|
-
return await updateComponent(space, existingId,
|
|
2638
|
+
return await updateComponent(space, existingId, payload);
|
|
2596
2639
|
} else {
|
|
2597
|
-
return await pushComponent(space,
|
|
2640
|
+
return await pushComponent(space, payload);
|
|
2598
2641
|
}
|
|
2599
2642
|
};
|
|
2600
2643
|
const pushComponentGroup = async (space, componentGroup) => {
|
|
@@ -2646,7 +2689,14 @@ const pushComponentPreset = async (space, preset) => {
|
|
|
2646
2689
|
space_id: Number(space)
|
|
2647
2690
|
},
|
|
2648
2691
|
body: {
|
|
2649
|
-
preset
|
|
2692
|
+
preset: {
|
|
2693
|
+
...preset,
|
|
2694
|
+
preset: preset.preset ?? void 0,
|
|
2695
|
+
image: preset.image ?? void 0,
|
|
2696
|
+
color: preset.color ?? void 0,
|
|
2697
|
+
icon: preset.icon ?? void 0,
|
|
2698
|
+
description: preset.description ?? void 0
|
|
2699
|
+
}
|
|
2650
2700
|
},
|
|
2651
2701
|
throwOnError: true
|
|
2652
2702
|
});
|
|
@@ -2663,7 +2713,14 @@ const updateComponentPreset = async (space, presetId, preset) => {
|
|
|
2663
2713
|
space_id: Number(space)
|
|
2664
2714
|
},
|
|
2665
2715
|
body: {
|
|
2666
|
-
preset
|
|
2716
|
+
preset: {
|
|
2717
|
+
...preset,
|
|
2718
|
+
preset: preset.preset ?? void 0,
|
|
2719
|
+
image: preset.image ?? void 0,
|
|
2720
|
+
color: preset.color ?? void 0,
|
|
2721
|
+
icon: preset.icon ?? void 0,
|
|
2722
|
+
description: preset.description ?? void 0
|
|
2723
|
+
}
|
|
2667
2724
|
},
|
|
2668
2725
|
throwOnError: true
|
|
2669
2726
|
});
|
|
@@ -2697,7 +2754,7 @@ const pushComponentInternalTag = async (space, componentInternalTag) => {
|
|
|
2697
2754
|
path: {
|
|
2698
2755
|
space_id: Number(space)
|
|
2699
2756
|
},
|
|
2700
|
-
body: componentInternalTag,
|
|
2757
|
+
body: { internal_tag: componentInternalTag },
|
|
2701
2758
|
throwOnError: true
|
|
2702
2759
|
});
|
|
2703
2760
|
return data.internal_tag;
|
|
@@ -2712,7 +2769,7 @@ const updateComponentInternalTag = async (space, tagId, componentInternalTag) =>
|
|
|
2712
2769
|
path: {
|
|
2713
2770
|
space_id: Number(space)
|
|
2714
2771
|
},
|
|
2715
|
-
body: componentInternalTag,
|
|
2772
|
+
body: { internal_tag: componentInternalTag },
|
|
2716
2773
|
throwOnError: true
|
|
2717
2774
|
});
|
|
2718
2775
|
return data.internal_tag;
|
|
@@ -3389,7 +3446,7 @@ function filterSpaceData(spaceData, selectors) {
|
|
|
3389
3446
|
if (filter && !minimatch(component.name, filter)) {
|
|
3390
3447
|
return false;
|
|
3391
3448
|
}
|
|
3392
|
-
if (groupUuids && !(component.component_group_uuid
|
|
3449
|
+
if (groupUuids && !(component.component_group_uuid != null && groupUuids.has(component.component_group_uuid))) {
|
|
3393
3450
|
return false;
|
|
3394
3451
|
}
|
|
3395
3452
|
if (tagIds) {
|
|
@@ -3453,7 +3510,7 @@ function resolveGroupSelector(groups, selector) {
|
|
|
3453
3510
|
while (p && !visited.has(p)) {
|
|
3454
3511
|
visited.add(p);
|
|
3455
3512
|
parts.unshift(nameOf(p) ?? p);
|
|
3456
|
-
p = byUuid.get(p)?.parent_uuid;
|
|
3513
|
+
p = byUuid.get(p)?.parent_uuid ?? null;
|
|
3457
3514
|
}
|
|
3458
3515
|
return parts.join("/");
|
|
3459
3516
|
});
|
|
@@ -4057,16 +4114,14 @@ const fetchSpace = async (spaceId) => {
|
|
|
4057
4114
|
handleAPIError("pull_spaces", error, `Failed to fetch space ${spaceId}`);
|
|
4058
4115
|
}
|
|
4059
4116
|
};
|
|
4060
|
-
const createSpace = async (space) => {
|
|
4117
|
+
const createSpace = async (space, query) => {
|
|
4061
4118
|
try {
|
|
4062
|
-
const { in_org, assign_partner, ...spaceData } = space;
|
|
4063
4119
|
const client = getMapiClient();
|
|
4064
4120
|
const { data } = await client.spaces.create({
|
|
4065
4121
|
body: {
|
|
4066
|
-
space
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
}
|
|
4122
|
+
space
|
|
4123
|
+
},
|
|
4124
|
+
query
|
|
4070
4125
|
});
|
|
4071
4126
|
return data?.space;
|
|
4072
4127
|
} catch (error) {
|
|
@@ -4077,10 +4132,10 @@ const createSpace = async (space) => {
|
|
|
4077
4132
|
const fetchLanguages = async (spaceId) => {
|
|
4078
4133
|
try {
|
|
4079
4134
|
const space = await fetchSpace(spaceId);
|
|
4080
|
-
if (space?.default_lang_name
|
|
4135
|
+
if (space?.default_lang_name && space?.languages?.length) {
|
|
4081
4136
|
return {
|
|
4082
|
-
default_lang_name: space
|
|
4083
|
-
languages: space
|
|
4137
|
+
default_lang_name: space.default_lang_name,
|
|
4138
|
+
languages: space.languages
|
|
4084
4139
|
};
|
|
4085
4140
|
}
|
|
4086
4141
|
} catch (error) {
|
|
@@ -4100,8 +4155,8 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
|
|
|
4100
4155
|
}
|
|
4101
4156
|
};
|
|
4102
4157
|
|
|
4103
|
-
const program$
|
|
4104
|
-
const languagesCommand = program$
|
|
4158
|
+
const program$a = getProgram();
|
|
4159
|
+
const languagesCommand = program$a.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`);
|
|
4105
4160
|
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
4161
|
pullCmd$3.action(async (options, command) => {
|
|
4107
4162
|
konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
|
|
@@ -4147,8 +4202,8 @@ pullCmd$3.action(async (options, command) => {
|
|
|
4147
4202
|
konsola.br();
|
|
4148
4203
|
});
|
|
4149
4204
|
|
|
4150
|
-
const program$
|
|
4151
|
-
const migrationsCommand = program$
|
|
4205
|
+
const program$9 = getProgram();
|
|
4206
|
+
const migrationsCommand = program$9.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`);
|
|
4152
4207
|
|
|
4153
4208
|
const getMigrationTemplate = () => {
|
|
4154
4209
|
return `export default function (block) {
|
|
@@ -4252,7 +4307,7 @@ const fetchStories = async (spaceId, params) => {
|
|
|
4252
4307
|
const fetchStory = async (spaceId, storyId) => {
|
|
4253
4308
|
try {
|
|
4254
4309
|
const client = getMapiClient();
|
|
4255
|
-
const { data } = await client.stories.get(storyId, {
|
|
4310
|
+
const { data } = await client.stories.get(Number(storyId), {
|
|
4256
4311
|
path: {
|
|
4257
4312
|
space_id: Number(spaceId)
|
|
4258
4313
|
},
|
|
@@ -4297,9 +4352,11 @@ const updateStory = async (spaceId, storyId, payload) => {
|
|
|
4297
4352
|
...payload.story,
|
|
4298
4353
|
// StoryUpdate2 expects `parent_id?: number`; normalize null → undefined.
|
|
4299
4354
|
parent_id: payload.story.parent_id ?? void 0
|
|
4300
|
-
}
|
|
4301
|
-
|
|
4302
|
-
|
|
4355
|
+
}
|
|
4356
|
+
},
|
|
4357
|
+
query: {
|
|
4358
|
+
force_update: payload.force_update === "1",
|
|
4359
|
+
...payload.publish ? { publish: Boolean(payload.publish) } : {}
|
|
4303
4360
|
},
|
|
4304
4361
|
throwOnError: true
|
|
4305
4362
|
});
|
|
@@ -4396,6 +4453,35 @@ const prefetchTargetStoriesByKeys = async (spaceId, keys, options) => {
|
|
|
4396
4453
|
return result;
|
|
4397
4454
|
};
|
|
4398
4455
|
|
|
4456
|
+
function parseFilterQuery(input) {
|
|
4457
|
+
const trimmed = input.trim();
|
|
4458
|
+
if (!trimmed) {
|
|
4459
|
+
return {};
|
|
4460
|
+
}
|
|
4461
|
+
if (trimmed.startsWith("{")) {
|
|
4462
|
+
return JSON.parse(trimmed);
|
|
4463
|
+
}
|
|
4464
|
+
const result = {};
|
|
4465
|
+
for (const clause of trimmed.split("&")) {
|
|
4466
|
+
if (!clause) {
|
|
4467
|
+
continue;
|
|
4468
|
+
}
|
|
4469
|
+
const eq = clause.indexOf("=");
|
|
4470
|
+
if (eq === -1) {
|
|
4471
|
+
continue;
|
|
4472
|
+
}
|
|
4473
|
+
const path = clause.slice(0, eq);
|
|
4474
|
+
const value = clause.slice(eq + 1);
|
|
4475
|
+
const keys = [...path.matchAll(/\[([^\]]+)\]/g)].map((match) => match[1]);
|
|
4476
|
+
if (keys.length < 2) {
|
|
4477
|
+
continue;
|
|
4478
|
+
}
|
|
4479
|
+
const [field, operation] = keys;
|
|
4480
|
+
result[field] = { ...result[field], [operation]: value };
|
|
4481
|
+
}
|
|
4482
|
+
return result;
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4399
4485
|
const PIPELINE_BACKPRESSURE_MULTIPLIER = 2;
|
|
4400
4486
|
const DEFAULT_PIPELINE_BACKPRESSURE = 12;
|
|
4401
4487
|
function createPipelineBackpressureLock(limit) {
|
|
@@ -4419,16 +4505,13 @@ const ERROR_CODES = {
|
|
|
4419
4505
|
async function* storiesIterator(spaceId, params, onTotal) {
|
|
4420
4506
|
try {
|
|
4421
4507
|
let perPage = 500;
|
|
4422
|
-
const
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
transformedParams.contain_component = params.componentName;
|
|
4427
|
-
delete transformedParams.componentName;
|
|
4508
|
+
const { componentName, query, ...rest } = params ?? {};
|
|
4509
|
+
const transformedParams = { ...rest };
|
|
4510
|
+
if (componentName) {
|
|
4511
|
+
transformedParams.contain_component = componentName;
|
|
4428
4512
|
}
|
|
4429
|
-
if (
|
|
4430
|
-
transformedParams.filter_query =
|
|
4431
|
-
delete transformedParams.query;
|
|
4513
|
+
if (query) {
|
|
4514
|
+
transformedParams.filter_query = parseFilterQuery(query);
|
|
4432
4515
|
}
|
|
4433
4516
|
const result = await fetchStories(spaceId, {
|
|
4434
4517
|
...transformedParams,
|
|
@@ -4753,8 +4836,8 @@ class MigrationStream extends Transform {
|
|
|
4753
4836
|
id: story.id,
|
|
4754
4837
|
name: story.name || "",
|
|
4755
4838
|
content: story.content,
|
|
4756
|
-
published: story.published,
|
|
4757
|
-
unpublished_changes: story.unpublished_changes
|
|
4839
|
+
published: story.published ?? void 0,
|
|
4840
|
+
unpublished_changes: story.unpublished_changes ?? void 0
|
|
4758
4841
|
},
|
|
4759
4842
|
migrationTimestamp: this.timestamp,
|
|
4760
4843
|
migrationNames
|
|
@@ -4770,8 +4853,8 @@ class MigrationStream extends Transform {
|
|
|
4770
4853
|
storyId: story.id,
|
|
4771
4854
|
name: story.name,
|
|
4772
4855
|
content: storyContent,
|
|
4773
|
-
published: story.published,
|
|
4774
|
-
unpublished_changes: story.unpublished_changes
|
|
4856
|
+
published: story.published ?? void 0,
|
|
4857
|
+
unpublished_changes: story.unpublished_changes ?? void 0
|
|
4775
4858
|
};
|
|
4776
4859
|
} else if (processed && !contentChanged) {
|
|
4777
4860
|
this.results.skipped.push({
|
|
@@ -4913,7 +4996,6 @@ class UpdateStream extends Writable {
|
|
|
4913
4996
|
const payload = {
|
|
4914
4997
|
story: {
|
|
4915
4998
|
content,
|
|
4916
|
-
id: storyId,
|
|
4917
4999
|
name: storyName
|
|
4918
5000
|
},
|
|
4919
5001
|
force_update: "1"
|
|
@@ -5162,7 +5244,6 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
|
|
|
5162
5244
|
const payload = {
|
|
5163
5245
|
story: {
|
|
5164
5246
|
content: story.content,
|
|
5165
|
-
id: story.storyId,
|
|
5166
5247
|
name: story.name
|
|
5167
5248
|
},
|
|
5168
5249
|
force_update: "1"
|
|
@@ -5202,8 +5283,8 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
|
|
|
5202
5283
|
}
|
|
5203
5284
|
});
|
|
5204
5285
|
|
|
5205
|
-
const program$
|
|
5206
|
-
const typesCommand = program$
|
|
5286
|
+
const program$8 = getProgram();
|
|
5287
|
+
const typesCommand = program$8.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`);
|
|
5207
5288
|
|
|
5208
5289
|
const getAssetJSONSchema = (title) => ({
|
|
5209
5290
|
$id: "#/asset",
|
|
@@ -6233,7 +6314,7 @@ const deleteDatasourceEntry = async (spaceId, entryId) => {
|
|
|
6233
6314
|
handleAPIError("delete_datasource_entry", error, `Failed to delete datasource entry ${entryId}`);
|
|
6234
6315
|
}
|
|
6235
6316
|
};
|
|
6236
|
-
function isDatasource(item) {
|
|
6317
|
+
function isDatasource$1(item) {
|
|
6237
6318
|
return typeof item === "object" && item !== null && "slug" in item && typeof item.slug === "string";
|
|
6238
6319
|
}
|
|
6239
6320
|
const readDatasourcesFiles = async (options) => {
|
|
@@ -6266,7 +6347,7 @@ const readDatasourcesFiles = async (options) => {
|
|
|
6266
6347
|
continue;
|
|
6267
6348
|
}
|
|
6268
6349
|
for (const item of data) {
|
|
6269
|
-
if (isDatasource(item)) {
|
|
6350
|
+
if (isDatasource$1(item)) {
|
|
6270
6351
|
const existing = datasourceMap.get(item.slug);
|
|
6271
6352
|
if (existing) {
|
|
6272
6353
|
duplicates.push(`Datasource "${item.slug}" found in both "${existing.file}" and "${file}"`);
|
|
@@ -6366,8 +6447,8 @@ generateCmd.action(async (options, command) => {
|
|
|
6366
6447
|
}
|
|
6367
6448
|
});
|
|
6368
6449
|
|
|
6369
|
-
const program$
|
|
6370
|
-
const datasourcesCommand = program$
|
|
6450
|
+
const program$7 = getProgram();
|
|
6451
|
+
const datasourcesCommand = program$7.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
|
|
6371
6452
|
|
|
6372
6453
|
const DEFAULT_DATASOURCES_FILENAME = "datasources";
|
|
6373
6454
|
|
|
@@ -6383,7 +6464,7 @@ const fetchDatasourceEntries = async (spaceId, datasourceId, dimensionId) => {
|
|
|
6383
6464
|
datasource_id: datasourceId,
|
|
6384
6465
|
page,
|
|
6385
6466
|
// MAPI matches `dimension` against the numeric dimension id.
|
|
6386
|
-
...dimensionId != null && { dimension:
|
|
6467
|
+
...dimensionId != null && { dimension: dimensionId }
|
|
6387
6468
|
},
|
|
6388
6469
|
throwOnError: true
|
|
6389
6470
|
}),
|
|
@@ -7092,13 +7173,13 @@ async function promptForLogin(verbose) {
|
|
|
7092
7173
|
return null;
|
|
7093
7174
|
}
|
|
7094
7175
|
}
|
|
7095
|
-
const program$
|
|
7096
|
-
program$
|
|
7176
|
+
const program$6 = getProgram();
|
|
7177
|
+
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(
|
|
7097
7178
|
"-r, --region <region>",
|
|
7098
7179
|
`The region to apply to the generated project template (does not affect space creation).`
|
|
7099
7180
|
).action(async (projectPath, options) => {
|
|
7100
7181
|
ui.title(`${commands.CREATE}`, colorPalette.CREATE);
|
|
7101
|
-
const verbose = program$
|
|
7182
|
+
const verbose = program$6.opts().verbose;
|
|
7102
7183
|
const { template, blueprint, token } = options;
|
|
7103
7184
|
if (options.region && !isRegion(options.region)) {
|
|
7104
7185
|
handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
|
|
@@ -7271,13 +7352,13 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
7271
7352
|
name: toHumanReadable(projectName),
|
|
7272
7353
|
domain: blueprintDomain
|
|
7273
7354
|
};
|
|
7355
|
+
const createSpaceQuery = {};
|
|
7274
7356
|
if (whereToCreateSpace === "org") {
|
|
7275
|
-
|
|
7276
|
-
spaceToCreate.in_org = true;
|
|
7357
|
+
createSpaceQuery.in_org = true;
|
|
7277
7358
|
} else if (whereToCreateSpace === "partner") {
|
|
7278
|
-
|
|
7359
|
+
createSpaceQuery.assign_partner = true;
|
|
7279
7360
|
}
|
|
7280
|
-
createdSpace = await createSpace(spaceToCreate);
|
|
7361
|
+
createdSpace = await createSpace(spaceToCreate, createSpaceQuery);
|
|
7281
7362
|
spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`);
|
|
7282
7363
|
if (createdSpace?.first_token) {
|
|
7283
7364
|
await handleEnvFileCreation(resolvedPath, createdSpace.first_token, region, technologyTemplate);
|
|
@@ -7317,8 +7398,8 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
7317
7398
|
ui.br();
|
|
7318
7399
|
});
|
|
7319
7400
|
|
|
7320
|
-
const program$
|
|
7321
|
-
const logsCommand = program$
|
|
7401
|
+
const program$5 = getProgram();
|
|
7402
|
+
const logsCommand = program$5.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`);
|
|
7322
7403
|
|
|
7323
7404
|
const listCmd$1 = logsCommand.command("list").description("List logs").option("-s, --space <space>", "space ID");
|
|
7324
7405
|
listCmd$1.action(async (_options, command) => {
|
|
@@ -7343,8 +7424,8 @@ pruneCmd$1.action(async (options, command) => {
|
|
|
7343
7424
|
ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
7344
7425
|
});
|
|
7345
7426
|
|
|
7346
|
-
const program$
|
|
7347
|
-
const reportsCommand = program$
|
|
7427
|
+
const program$4 = getProgram();
|
|
7428
|
+
const reportsCommand = program$4.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.");
|
|
7348
7429
|
|
|
7349
7430
|
const listCmd = reportsCommand.command("list").description("List reports").option("-s, --space <space>", "space ID");
|
|
7350
7431
|
listCmd.action(async (_options, command) => {
|
|
@@ -7369,8 +7450,8 @@ pruneCmd.action(async (options, command) => {
|
|
|
7369
7450
|
ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
7370
7451
|
});
|
|
7371
7452
|
|
|
7372
|
-
const program$
|
|
7373
|
-
const assetsCommand = program$
|
|
7453
|
+
const program$3 = getProgram();
|
|
7454
|
+
const assetsCommand = program$3.command(commands.ASSETS).description(`Manage your space's assets`);
|
|
7374
7455
|
|
|
7375
7456
|
const fetchAssets = async ({ spaceId, params }) => {
|
|
7376
7457
|
try {
|
|
@@ -7418,7 +7499,7 @@ const createAssetInternalTag = async (spaceId, name) => {
|
|
|
7418
7499
|
const client = getMapiClient();
|
|
7419
7500
|
const { data } = await client.internalTags.create({
|
|
7420
7501
|
path: { space_id: Number(spaceId) },
|
|
7421
|
-
body: { name, object_type: "asset" },
|
|
7502
|
+
body: { internal_tag: { name, object_type: "asset" } },
|
|
7422
7503
|
throwOnError: true
|
|
7423
7504
|
});
|
|
7424
7505
|
const tag = data?.internal_tag;
|
|
@@ -7542,7 +7623,7 @@ const updateAsset = async (id, asset, { spaceId, fileBuffer }) => {
|
|
|
7542
7623
|
const createAsset = async (asset, fileBuffer, { spaceId }) => {
|
|
7543
7624
|
try {
|
|
7544
7625
|
const client = getMapiClient();
|
|
7545
|
-
const { id: _id, ...assetBody } = asset;
|
|
7626
|
+
const { id: _id, filename: _filename, ...assetBody } = asset;
|
|
7546
7627
|
return await client.assets.create({
|
|
7547
7628
|
body: assetBody,
|
|
7548
7629
|
file: fileBuffer,
|
|
@@ -7762,6 +7843,26 @@ const parseAssetData = (raw) => {
|
|
|
7762
7843
|
throw new Error(`Invalid --data JSON: ${toError(maybeError).message}`);
|
|
7763
7844
|
}
|
|
7764
7845
|
};
|
|
7846
|
+
const toAssetUpload = (partial, shortFilename) => {
|
|
7847
|
+
const nullToUndef = (value) => value ?? void 0;
|
|
7848
|
+
return {
|
|
7849
|
+
id: partial.id,
|
|
7850
|
+
filename: partial.filename,
|
|
7851
|
+
short_filename: shortFilename,
|
|
7852
|
+
asset_folder_id: nullToUndef(partial.asset_folder_id),
|
|
7853
|
+
ext_id: nullToUndef(partial.ext_id),
|
|
7854
|
+
alt: nullToUndef(partial.alt),
|
|
7855
|
+
copyright: nullToUndef(partial.copyright),
|
|
7856
|
+
title: nullToUndef(partial.title),
|
|
7857
|
+
source: nullToUndef(partial.source),
|
|
7858
|
+
expire_at: nullToUndef(partial.expire_at),
|
|
7859
|
+
publish_at: nullToUndef(partial.publish_at),
|
|
7860
|
+
focus: nullToUndef(partial.focus),
|
|
7861
|
+
is_private: nullToUndef(partial.is_private),
|
|
7862
|
+
internal_tag_ids: partial.internal_tag_ids,
|
|
7863
|
+
meta_data: nullToUndef(partial.meta_data)
|
|
7864
|
+
};
|
|
7865
|
+
};
|
|
7765
7866
|
const getSidecarFilename = (assetBinaryPath) => {
|
|
7766
7867
|
return join(dirname(assetBinaryPath), `${basename(assetBinaryPath, extname(assetBinaryPath))}.json`);
|
|
7767
7868
|
};
|
|
@@ -7790,7 +7891,7 @@ const isRemoteSource = (assetBinaryPath) => {
|
|
|
7790
7891
|
return false;
|
|
7791
7892
|
}
|
|
7792
7893
|
};
|
|
7793
|
-
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.
|
|
7894
|
+
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.new_filename);
|
|
7794
7895
|
const loadAssetMap = async (manifestFile) => {
|
|
7795
7896
|
const manifest = await loadManifest(manifestFile);
|
|
7796
7897
|
const entries = manifest.filter(isValidManifestEntry).map((e) => [
|
|
@@ -8219,8 +8320,10 @@ const readLocalAssetsStream = ({
|
|
|
8219
8320
|
const sidecar = await loadSidecarAssetData(binaryFilePath);
|
|
8220
8321
|
const shortFilename = sidecar.short_filename || (sidecar.filename ? basename(sidecar.filename) : void 0) || file;
|
|
8221
8322
|
const asset = {
|
|
8222
|
-
...sidecar,
|
|
8223
|
-
|
|
8323
|
+
...toAssetUpload(sidecar, shortFilename),
|
|
8324
|
+
// Carry the read-only tag detail for source→target tag-name
|
|
8325
|
+
// translation in `processAsset`; it is stripped before the API call.
|
|
8326
|
+
...sidecar.internal_tags_list ? { internal_tags_list: sidecar.internal_tags_list } : {}
|
|
8224
8327
|
};
|
|
8225
8328
|
const fileBuffer = await readFile$1(binaryFilePath);
|
|
8226
8329
|
const sidecarPath = getSidecarFilename(binaryFilePath);
|
|
@@ -8304,7 +8407,7 @@ const mapInternalTagIds = (sourceIds, sourceTags, assetInternalTagsByName, onUnm
|
|
|
8304
8407
|
const sourceName = sourceNamesById.get(sourceId);
|
|
8305
8408
|
const targetId = typeof sourceName === "string" ? assetInternalTagsByName.get(sourceName) : void 0;
|
|
8306
8409
|
if (typeof targetId === "number") {
|
|
8307
|
-
mapped.push(
|
|
8410
|
+
mapped.push(targetId);
|
|
8308
8411
|
} else {
|
|
8309
8412
|
onUnmappedTag?.({ sourceId, name: sourceName });
|
|
8310
8413
|
}
|
|
@@ -8358,12 +8461,13 @@ const processAsset = async ({
|
|
|
8358
8461
|
if (maps.resolveSharedTagIds) {
|
|
8359
8462
|
return maps.resolveSharedTagIds(sourceIds, sourceTags);
|
|
8360
8463
|
}
|
|
8361
|
-
return maps.assetInternalTagsByName ? mapInternalTagIds(sourceIds, sourceTags, maps.assetInternalTagsByName, onUnmappedTag) : (sourceIds ?? []).map((id) =>
|
|
8464
|
+
return maps.assetInternalTagsByName ? mapInternalTagIds(sourceIds, sourceTags, maps.assetInternalTagsByName, onUnmappedTag) : (sourceIds ?? []).map((id) => Number(id));
|
|
8362
8465
|
};
|
|
8363
8466
|
let newRemoteAsset;
|
|
8364
8467
|
let status;
|
|
8365
8468
|
if (remoteAsset) {
|
|
8366
|
-
const updateTagIds =
|
|
8469
|
+
const updateTagIds = localAsset.internal_tag_ids ? await resolveInternalTagIds(localAsset.internal_tag_ids) : remoteAsset.internal_tag_ids;
|
|
8470
|
+
const nullToUndef = (v) => v ?? void 0;
|
|
8367
8471
|
const updatePayload = {
|
|
8368
8472
|
asset_folder_id: remoteFolderId,
|
|
8369
8473
|
alt: "alt" in localAsset ? localAsset.alt : remoteAsset.alt,
|
|
@@ -8372,25 +8476,29 @@ const processAsset = async ({
|
|
|
8372
8476
|
source: "source" in localAsset ? localAsset.source : remoteAsset.source,
|
|
8373
8477
|
is_private: "is_private" in localAsset ? localAsset.is_private : remoteAsset.is_private,
|
|
8374
8478
|
focus: "focus" in localAsset ? localAsset.focus : remoteAsset.focus,
|
|
8375
|
-
expire_at: "expire_at" in localAsset ? localAsset.expire_at : remoteAsset.expire_at,
|
|
8376
|
-
publish_at: "publish_at" in localAsset ? localAsset.publish_at : remoteAsset.publish_at,
|
|
8479
|
+
expire_at: nullToUndef("expire_at" in localAsset ? localAsset.expire_at : remoteAsset.expire_at),
|
|
8480
|
+
publish_at: nullToUndef("publish_at" in localAsset ? localAsset.publish_at : remoteAsset.publish_at),
|
|
8377
8481
|
internal_tag_ids: updateTagIds,
|
|
8378
|
-
meta_data: "meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data
|
|
8482
|
+
meta_data: nullToUndef("meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data)
|
|
8379
8483
|
};
|
|
8380
8484
|
await transports.updateAsset(
|
|
8381
8485
|
remoteAsset.id,
|
|
8382
8486
|
{ ...updatePayload, short_filename: remoteAsset.short_filename },
|
|
8383
8487
|
fileBuffer
|
|
8384
8488
|
);
|
|
8385
|
-
newRemoteAsset = {
|
|
8489
|
+
newRemoteAsset = {
|
|
8490
|
+
...remoteAsset,
|
|
8491
|
+
...updatePayload,
|
|
8492
|
+
is_private: updatePayload.is_private ?? remoteAsset.is_private
|
|
8493
|
+
};
|
|
8386
8494
|
status = "updated";
|
|
8387
8495
|
} else if (hasProp(localAsset, "short_filename")) {
|
|
8388
|
-
const
|
|
8389
|
-
const
|
|
8390
|
-
const
|
|
8496
|
+
const mappedTagIds = localAsset.internal_tag_ids ? await resolveInternalTagIds(localAsset.internal_tag_ids) : void 0;
|
|
8497
|
+
const size = hasProp(localAsset, "size") ? localAsset.size : hasProp(localAsset, "filename") ? extractAssetSizeFromFilename(localAsset.filename) : void 0;
|
|
8498
|
+
const { internal_tag_ids: _sourceTagIds, ...uploadBase } = toAssetUpload(localAsset, localAsset.short_filename);
|
|
8391
8499
|
const createPayload = {
|
|
8392
|
-
...
|
|
8393
|
-
asset_folder_id: remoteFolderId,
|
|
8500
|
+
...uploadBase,
|
|
8501
|
+
asset_folder_id: remoteFolderId ?? void 0,
|
|
8394
8502
|
...mappedTagIds !== void 0 ? { internal_tag_ids: mappedTagIds } : {},
|
|
8395
8503
|
...size !== void 0 ? { size } : {}
|
|
8396
8504
|
};
|
|
@@ -8506,7 +8614,7 @@ const makeSharedTagResolver = ({ spaceId, libraryId }) => {
|
|
|
8506
8614
|
}
|
|
8507
8615
|
}
|
|
8508
8616
|
if (sharedId !== void 0) {
|
|
8509
|
-
remapped.push(
|
|
8617
|
+
remapped.push(sharedId);
|
|
8510
8618
|
}
|
|
8511
8619
|
}
|
|
8512
8620
|
return remapped;
|
|
@@ -8916,8 +9024,8 @@ const traverseAndMapBySchema = (data, {
|
|
|
8916
9024
|
const dataNew = { ...data };
|
|
8917
9025
|
for (const [fieldName, fieldValue] of Object.entries(data)) {
|
|
8918
9026
|
const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
|
|
8919
|
-
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema
|
|
8920
|
-
const fieldRefMapper = typeof fieldType === "string"
|
|
9027
|
+
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema ? fieldSchema.type : void 0;
|
|
9028
|
+
const fieldRefMapper = typeof fieldType === "string" ? fieldRefMappers2[fieldType] : void 0;
|
|
8921
9029
|
if (fieldRefMapper) {
|
|
8922
9030
|
dataNew[fieldName] = fieldRefMapper(fieldValue, {
|
|
8923
9031
|
schema: fieldSchema,
|
|
@@ -8982,6 +9090,9 @@ const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRef
|
|
|
8982
9090
|
fieldRefMappers: fieldRefMappers2
|
|
8983
9091
|
});
|
|
8984
9092
|
const multilinkFieldRefMapper = (data, { maps }) => {
|
|
9093
|
+
if (!data || typeof data !== "object") {
|
|
9094
|
+
return data;
|
|
9095
|
+
}
|
|
8985
9096
|
if (data.linktype !== "story") {
|
|
8986
9097
|
return data;
|
|
8987
9098
|
}
|
|
@@ -9001,6 +9112,9 @@ const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMap
|
|
|
9001
9112
|
}));
|
|
9002
9113
|
};
|
|
9003
9114
|
const assetFieldRefMapper = (data, { maps }) => {
|
|
9115
|
+
if (!data || typeof data !== "object") {
|
|
9116
|
+
return data;
|
|
9117
|
+
}
|
|
9004
9118
|
const mappedAsset = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
|
|
9005
9119
|
if (!mappedAsset) {
|
|
9006
9120
|
return data;
|
|
@@ -9018,7 +9132,7 @@ const multiassetFieldRefMapper = (data, options) => {
|
|
|
9018
9132
|
return data.map((d) => assetFieldRefMapper(d, options));
|
|
9019
9133
|
};
|
|
9020
9134
|
const optionsFieldRefMapper = (data, { schema, maps }) => {
|
|
9021
|
-
if (schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
9135
|
+
if (!schema || !("source" in schema) || schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
9022
9136
|
return data;
|
|
9023
9137
|
}
|
|
9024
9138
|
return data.map((d) => maps.stories?.get(d) || d);
|
|
@@ -9911,11 +10025,7 @@ pushCmd$1.action(async (assetInput, options, command) => {
|
|
|
9911
10025
|
const sourceBasename = isRemoteSource(assetBinaryPath) ? basename(new URL(assetBinaryPath).pathname) : basename(assetBinaryPath);
|
|
9912
10026
|
const shortFilename = options.shortFilename || assetDataPartial.short_filename || sourceBasename;
|
|
9913
10027
|
const folderId = options.folder ? Number(options.folder) : void 0;
|
|
9914
|
-
assetData = {
|
|
9915
|
-
...assetDataPartial,
|
|
9916
|
-
short_filename: shortFilename,
|
|
9917
|
-
asset_folder_id: folderId
|
|
9918
|
-
};
|
|
10028
|
+
assetData = { ...toAssetUpload(assetDataPartial, shortFilename), asset_folder_id: folderId };
|
|
9919
10029
|
}
|
|
9920
10030
|
try {
|
|
9921
10031
|
for (const scope of scopes) {
|
|
@@ -10123,8 +10233,8 @@ transferCmd.action(async (assetIds, options, command) => {
|
|
|
10123
10233
|
process.exitCode = summary.failed > 0 ? 1 : 0;
|
|
10124
10234
|
});
|
|
10125
10235
|
|
|
10126
|
-
const program$
|
|
10127
|
-
const storiesCommand = program$
|
|
10236
|
+
const program$2 = getProgram();
|
|
10237
|
+
const storiesCommand = program$2.command(commands.STORIES).description(`Manage your space's stories`);
|
|
10128
10238
|
|
|
10129
10239
|
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.`);
|
|
10130
10240
|
pullCmd.action(async (options, command) => {
|
|
@@ -10160,7 +10270,7 @@ pullCmd.action(async (options, command) => {
|
|
|
10160
10270
|
fetchStoriesStream({
|
|
10161
10271
|
spaceId: space,
|
|
10162
10272
|
params: {
|
|
10163
|
-
filter_query: options.query,
|
|
10273
|
+
filter_query: options.query ? parseFilterQuery(options.query) : void 0,
|
|
10164
10274
|
starts_with: options.startsWith
|
|
10165
10275
|
},
|
|
10166
10276
|
setTotalPages: (totalPages) => {
|
|
@@ -10629,6 +10739,2337 @@ pushCmd.action(async (options, command) => {
|
|
|
10629
10739
|
}
|
|
10630
10740
|
});
|
|
10631
10741
|
|
|
10742
|
+
const program$1 = getProgram();
|
|
10743
|
+
const schemaCommand = program$1.command(commands.SCHEMA).description(`Manage your space's schema from code`);
|
|
10744
|
+
|
|
10745
|
+
function isRecord(value) {
|
|
10746
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10747
|
+
}
|
|
10748
|
+
const COMPONENT_STRIP_KEYS = /* @__PURE__ */ new Set([
|
|
10749
|
+
"id",
|
|
10750
|
+
"created_at",
|
|
10751
|
+
"updated_at",
|
|
10752
|
+
"real_name",
|
|
10753
|
+
// API-computed display/technical name, read-only
|
|
10754
|
+
"preset_id",
|
|
10755
|
+
// Instance-level preset selection, not part of schema definition
|
|
10756
|
+
"all_presets",
|
|
10757
|
+
// Computed list of presets, managed via /presets API
|
|
10758
|
+
"internal_tags_list",
|
|
10759
|
+
// Read-only expanded form of internal_tag_ids ({id, name} objects)
|
|
10760
|
+
"content_type_asset_preview",
|
|
10761
|
+
// Read-only, not in ComponentCreate/ComponentUpdate
|
|
10762
|
+
"image",
|
|
10763
|
+
// Read-only preview image URL
|
|
10764
|
+
"preview_tmpl",
|
|
10765
|
+
// Read-only preview template
|
|
10766
|
+
"metadata",
|
|
10767
|
+
// Not in current API types, stripped defensively
|
|
10768
|
+
"component_group_uuid"
|
|
10769
|
+
// UI grouping; stripped by default — kept for diffing only when a block opts into the group escape hatch
|
|
10770
|
+
]);
|
|
10771
|
+
const DATASOURCE_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
|
|
10772
|
+
const DATASOURCE_DIMENSION_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "datasource_id", "created_at", "updated_at"]);
|
|
10773
|
+
const DATASOURCE_DEFAULTS = {
|
|
10774
|
+
dimensions: []
|
|
10775
|
+
};
|
|
10776
|
+
const COMPONENT_DEFAULTS = {
|
|
10777
|
+
display_name: "",
|
|
10778
|
+
description: "",
|
|
10779
|
+
color: "",
|
|
10780
|
+
icon: "",
|
|
10781
|
+
preview_field: "",
|
|
10782
|
+
internal_tag_ids: []
|
|
10783
|
+
};
|
|
10784
|
+
function applyDefaults(entity, defaults) {
|
|
10785
|
+
const result = { ...entity };
|
|
10786
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
10787
|
+
if (result[key] === void 0 || result[key] === null) {
|
|
10788
|
+
Object.assign(result, { [key]: defaultValue });
|
|
10789
|
+
}
|
|
10790
|
+
}
|
|
10791
|
+
return result;
|
|
10792
|
+
}
|
|
10793
|
+
const INDENT = " ";
|
|
10794
|
+
class RawCode {
|
|
10795
|
+
constructor(code) {
|
|
10796
|
+
this.code = code;
|
|
10797
|
+
}
|
|
10798
|
+
}
|
|
10799
|
+
function quoteString(value) {
|
|
10800
|
+
const escaped = JSON.stringify(value).slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'");
|
|
10801
|
+
return `'${escaped}'`;
|
|
10802
|
+
}
|
|
10803
|
+
function formatValue(value, depth) {
|
|
10804
|
+
const indent = INDENT.repeat(depth);
|
|
10805
|
+
const innerIndent = INDENT.repeat(depth + 1);
|
|
10806
|
+
if (value === null || value === void 0) {
|
|
10807
|
+
return String(value);
|
|
10808
|
+
}
|
|
10809
|
+
if (value instanceof RawCode) {
|
|
10810
|
+
return value.code;
|
|
10811
|
+
}
|
|
10812
|
+
if (typeof value === "string") {
|
|
10813
|
+
return quoteString(value);
|
|
10814
|
+
}
|
|
10815
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
10816
|
+
return String(value);
|
|
10817
|
+
}
|
|
10818
|
+
if (Array.isArray(value)) {
|
|
10819
|
+
if (value.length === 0) {
|
|
10820
|
+
return "[]";
|
|
10821
|
+
}
|
|
10822
|
+
const items = value.map((item) => `${innerIndent}${formatValue(item, depth + 1)},`);
|
|
10823
|
+
return `[
|
|
10824
|
+
${items.join("\n")}
|
|
10825
|
+
${indent}]`;
|
|
10826
|
+
}
|
|
10827
|
+
if (typeof value === "object") {
|
|
10828
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0 && v !== null).sort(([a], [b]) => a.localeCompare(b));
|
|
10829
|
+
if (entries.length === 0) {
|
|
10830
|
+
return "{}";
|
|
10831
|
+
}
|
|
10832
|
+
const props = entries.map(
|
|
10833
|
+
([key, val]) => `${innerIndent}${key}: ${formatValue(val, depth + 1)},`
|
|
10834
|
+
);
|
|
10835
|
+
return `{
|
|
10836
|
+
${props.join("\n")}
|
|
10837
|
+
${indent}}`;
|
|
10838
|
+
}
|
|
10839
|
+
return String(value);
|
|
10840
|
+
}
|
|
10841
|
+
function fileTimestamp(iso) {
|
|
10842
|
+
return iso.replace(/\D/g, "").slice(0, 14);
|
|
10843
|
+
}
|
|
10844
|
+
function displayPath(filePath, userPath) {
|
|
10845
|
+
return userPath && isAbsolute(userPath) ? filePath : relative(process.cwd(), filePath);
|
|
10846
|
+
}
|
|
10847
|
+
function stripKeys(obj, keysToStrip) {
|
|
10848
|
+
const result = {};
|
|
10849
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
10850
|
+
if (!keysToStrip.has(key) && value !== void 0 && value !== null) {
|
|
10851
|
+
result[key] = value;
|
|
10852
|
+
}
|
|
10853
|
+
}
|
|
10854
|
+
return result;
|
|
10855
|
+
}
|
|
10856
|
+
|
|
10857
|
+
function buildGroupPathByUuid(folders) {
|
|
10858
|
+
const byUuid = new Map(folders.map((folder) => [folder.uuid, folder]));
|
|
10859
|
+
const cache = /* @__PURE__ */ new Map();
|
|
10860
|
+
function pathFor(uuid, visited) {
|
|
10861
|
+
if (!uuid) {
|
|
10862
|
+
return [];
|
|
10863
|
+
}
|
|
10864
|
+
const cached = cache.get(uuid);
|
|
10865
|
+
if (cached) {
|
|
10866
|
+
return cached;
|
|
10867
|
+
}
|
|
10868
|
+
const folder = byUuid.get(uuid);
|
|
10869
|
+
if (!folder) {
|
|
10870
|
+
return [];
|
|
10871
|
+
}
|
|
10872
|
+
if (visited.has(uuid)) {
|
|
10873
|
+
return [];
|
|
10874
|
+
}
|
|
10875
|
+
visited.add(uuid);
|
|
10876
|
+
const path = [...pathFor(folder.parent_uuid, visited), slugify(folder.name)];
|
|
10877
|
+
cache.set(uuid, path);
|
|
10878
|
+
return path;
|
|
10879
|
+
}
|
|
10880
|
+
for (const folder of folders) {
|
|
10881
|
+
pathFor(folder.uuid, /* @__PURE__ */ new Set());
|
|
10882
|
+
}
|
|
10883
|
+
return cache;
|
|
10884
|
+
}
|
|
10885
|
+
function slugifyPath(displayPath) {
|
|
10886
|
+
return displayPath.split("/").map((segment) => slugify(segment)).filter(Boolean).join("/");
|
|
10887
|
+
}
|
|
10888
|
+
function expandFolderPath(displayPath) {
|
|
10889
|
+
const segments = displayPath.split("/").map((segment) => ({ name: segment, slug: slugify(segment) })).filter((segment) => segment.slug !== "");
|
|
10890
|
+
const result = [];
|
|
10891
|
+
let parentPath = null;
|
|
10892
|
+
for (const { name, slug } of segments) {
|
|
10893
|
+
const path = parentPath ? `${parentPath}/${slug}` : slug;
|
|
10894
|
+
result.push({ name, path, parentPath });
|
|
10895
|
+
parentPath = path;
|
|
10896
|
+
}
|
|
10897
|
+
return result;
|
|
10898
|
+
}
|
|
10899
|
+
|
|
10900
|
+
function mapFieldToWire(field) {
|
|
10901
|
+
const { name, allow, datasource, ...rest } = field;
|
|
10902
|
+
const value = { ...rest };
|
|
10903
|
+
if (allow !== void 0 && Array.isArray(allow)) {
|
|
10904
|
+
const folderPaths = allow.filter((entry) => isRecord(entry) && typeof entry.folder === "string").map((entry) => slugifyPath(entry.folder));
|
|
10905
|
+
const blockNames = allow.filter((entry) => typeof entry === "string");
|
|
10906
|
+
if (folderPaths.length > 0) {
|
|
10907
|
+
value.component_group_whitelist = folderPaths;
|
|
10908
|
+
if (rest.type === "bloks" || rest.type === "richtext") {
|
|
10909
|
+
value.restrict_components = true;
|
|
10910
|
+
value.restrict_type = "groups";
|
|
10911
|
+
}
|
|
10912
|
+
} else {
|
|
10913
|
+
value.component_whitelist = blockNames;
|
|
10914
|
+
if (rest.type === "bloks") {
|
|
10915
|
+
value.restrict_components = true;
|
|
10916
|
+
value.restrict_type = "";
|
|
10917
|
+
}
|
|
10918
|
+
}
|
|
10919
|
+
} else if (allow !== void 0) {
|
|
10920
|
+
value.component_whitelist = allow;
|
|
10921
|
+
if (rest.type === "bloks") {
|
|
10922
|
+
value.restrict_components = true;
|
|
10923
|
+
value.restrict_type = "";
|
|
10924
|
+
}
|
|
10925
|
+
}
|
|
10926
|
+
if (datasource !== void 0) {
|
|
10927
|
+
value.datasource_slug = datasource;
|
|
10928
|
+
}
|
|
10929
|
+
return { name: typeof name === "string" ? name : "", value };
|
|
10930
|
+
}
|
|
10931
|
+
function mapBlockToWire(block) {
|
|
10932
|
+
const { fields, folder, ...rest } = block;
|
|
10933
|
+
const schema = {};
|
|
10934
|
+
if (Array.isArray(fields)) {
|
|
10935
|
+
for (const field of fields) {
|
|
10936
|
+
if (!isRecord(field)) {
|
|
10937
|
+
continue;
|
|
10938
|
+
}
|
|
10939
|
+
const { name, value } = mapFieldToWire(field);
|
|
10940
|
+
if (name) {
|
|
10941
|
+
schema[name] = value;
|
|
10942
|
+
}
|
|
10943
|
+
}
|
|
10944
|
+
}
|
|
10945
|
+
return {
|
|
10946
|
+
...rest,
|
|
10947
|
+
...folder !== void 0 && { folder: typeof folder === "string" ? slugifyPath(folder) : null },
|
|
10948
|
+
schema
|
|
10949
|
+
};
|
|
10950
|
+
}
|
|
10951
|
+
function mapDatasourceToWire(datasource) {
|
|
10952
|
+
return datasource;
|
|
10953
|
+
}
|
|
10954
|
+
|
|
10955
|
+
function isComponent(value) {
|
|
10956
|
+
return isRecord(value) && typeof value.name === "string" && Array.isArray(value.fields);
|
|
10957
|
+
}
|
|
10958
|
+
function isDatasource(value) {
|
|
10959
|
+
return isRecord(value) && typeof value.name === "string" && typeof value.slug === "string" && !Array.isArray(value.fields);
|
|
10960
|
+
}
|
|
10961
|
+
function isFolder(value) {
|
|
10962
|
+
return isRecord(value) && typeof value.name === "string" && typeof value.path === "string" && !Array.isArray(value.fields) && !("slug" in value);
|
|
10963
|
+
}
|
|
10964
|
+
function buildLocalFolders(registered, derived) {
|
|
10965
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
10966
|
+
const add = (displayPath, registeredLeaf) => {
|
|
10967
|
+
const expanded = expandFolderPath(displayPath);
|
|
10968
|
+
expanded.forEach((entry, i) => {
|
|
10969
|
+
const isLeaf = i === expanded.length - 1;
|
|
10970
|
+
const isRegisteredEntry = registeredLeaf && isLeaf;
|
|
10971
|
+
const existing = byPath.get(entry.path);
|
|
10972
|
+
if (!existing) {
|
|
10973
|
+
byPath.set(entry.path, { folder: entry, isRegistered: isRegisteredEntry });
|
|
10974
|
+
return;
|
|
10975
|
+
}
|
|
10976
|
+
if (isRegisteredEntry && existing.isRegistered && existing.folder.name !== entry.name) {
|
|
10977
|
+
throw new CommandError(`Conflicting folder names for path "${entry.path}": "${existing.folder.name}" vs "${entry.name}"`);
|
|
10978
|
+
}
|
|
10979
|
+
if (isRegisteredEntry && !existing.isRegistered) {
|
|
10980
|
+
byPath.set(entry.path, { folder: entry, isRegistered: true });
|
|
10981
|
+
}
|
|
10982
|
+
});
|
|
10983
|
+
};
|
|
10984
|
+
for (const path of registered) {
|
|
10985
|
+
add(path, true);
|
|
10986
|
+
}
|
|
10987
|
+
for (const path of derived) {
|
|
10988
|
+
add(path, false);
|
|
10989
|
+
}
|
|
10990
|
+
const folders = [...byPath.values()].map((v) => v.folder).sort((a, b) => a.path.split("/").length - b.path.split("/").length || a.path.localeCompare(b.path));
|
|
10991
|
+
const leafToPath = /* @__PURE__ */ new Map();
|
|
10992
|
+
for (const folder of folders) {
|
|
10993
|
+
const leaf = folder.path.split("/").pop() ?? folder.path;
|
|
10994
|
+
const existing = leafToPath.get(leaf);
|
|
10995
|
+
if (existing !== void 0 && existing !== folder.path) {
|
|
10996
|
+
throw new CommandError(
|
|
10997
|
+
`Duplicate folder name "${leaf}" (folders "${existing}" and "${folder.path}"): Storyblok group names must be unique per space, even under different parents.`
|
|
10998
|
+
);
|
|
10999
|
+
}
|
|
11000
|
+
leafToPath.set(leaf, folder.path);
|
|
11001
|
+
}
|
|
11002
|
+
return folders;
|
|
11003
|
+
}
|
|
11004
|
+
function isSchemaObject(value) {
|
|
11005
|
+
return isRecord(value) && ("blocks" in value || "datasources" in value || "folders" in value);
|
|
11006
|
+
}
|
|
11007
|
+
function emptySchemaData() {
|
|
11008
|
+
return { components: [], folders: [], datasources: [] };
|
|
11009
|
+
}
|
|
11010
|
+
function classifyExports(moduleExports) {
|
|
11011
|
+
const data = emptySchemaData();
|
|
11012
|
+
const seenComponents = /* @__PURE__ */ new Set();
|
|
11013
|
+
const seenDatasources = /* @__PURE__ */ new Set();
|
|
11014
|
+
const registered = [];
|
|
11015
|
+
const derived = [];
|
|
11016
|
+
function harvestComponentPaths(value) {
|
|
11017
|
+
if (typeof value.folder === "string") {
|
|
11018
|
+
derived.push(value.folder);
|
|
11019
|
+
}
|
|
11020
|
+
if (Array.isArray(value.fields)) {
|
|
11021
|
+
for (const field of value.fields) {
|
|
11022
|
+
if (!isRecord(field) || !Array.isArray(field.allow)) {
|
|
11023
|
+
continue;
|
|
11024
|
+
}
|
|
11025
|
+
for (const entry of field.allow) {
|
|
11026
|
+
if (isRecord(entry) && typeof entry.folder === "string") {
|
|
11027
|
+
derived.push(entry.folder);
|
|
11028
|
+
}
|
|
11029
|
+
}
|
|
11030
|
+
}
|
|
11031
|
+
}
|
|
11032
|
+
}
|
|
11033
|
+
function collect(value) {
|
|
11034
|
+
if (isComponent(value)) {
|
|
11035
|
+
harvestComponentPaths(value);
|
|
11036
|
+
if (seenComponents.has(value.name)) {
|
|
11037
|
+
return;
|
|
11038
|
+
}
|
|
11039
|
+
seenComponents.add(value.name);
|
|
11040
|
+
data.components.push(mapBlockToWire(value));
|
|
11041
|
+
} else if (isFolder(value)) {
|
|
11042
|
+
registered.push(value.path);
|
|
11043
|
+
} else if (isDatasource(value)) {
|
|
11044
|
+
if (seenDatasources.has(value.name)) {
|
|
11045
|
+
return;
|
|
11046
|
+
}
|
|
11047
|
+
seenDatasources.add(value.name);
|
|
11048
|
+
data.datasources.push(mapDatasourceToWire(value));
|
|
11049
|
+
}
|
|
11050
|
+
}
|
|
11051
|
+
for (const value of Object.values(moduleExports)) {
|
|
11052
|
+
if (isSchemaObject(value)) {
|
|
11053
|
+
for (const group of Object.values(value)) {
|
|
11054
|
+
if (isRecord(group)) {
|
|
11055
|
+
for (const entity of Object.values(group)) {
|
|
11056
|
+
collect(entity);
|
|
11057
|
+
}
|
|
11058
|
+
}
|
|
11059
|
+
}
|
|
11060
|
+
} else {
|
|
11061
|
+
collect(value);
|
|
11062
|
+
}
|
|
11063
|
+
}
|
|
11064
|
+
data.folders = buildLocalFolders(registered, derived);
|
|
11065
|
+
return data;
|
|
11066
|
+
}
|
|
11067
|
+
async function loadSchema(entryPath) {
|
|
11068
|
+
const { createJiti } = await import('jiti');
|
|
11069
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
11070
|
+
const { resolve } = await import('pathe');
|
|
11071
|
+
const entryAbs = resolve(entryPath);
|
|
11072
|
+
const entryMod = await jiti.import(entryAbs);
|
|
11073
|
+
return classifyExports(entryMod);
|
|
11074
|
+
}
|
|
11075
|
+
|
|
11076
|
+
function sortSchemaByPos$1(schema) {
|
|
11077
|
+
const entries = Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
|
|
11078
|
+
const posA = typeof a.pos === "number" ? a.pos : Infinity;
|
|
11079
|
+
const posB = typeof b.pos === "number" ? b.pos : Infinity;
|
|
11080
|
+
return posA - posB;
|
|
11081
|
+
});
|
|
11082
|
+
return Object.fromEntries(
|
|
11083
|
+
entries.map(([key, field]) => {
|
|
11084
|
+
const { id, ...rest } = field;
|
|
11085
|
+
if (rest.restrict_type === "") {
|
|
11086
|
+
delete rest.restrict_type;
|
|
11087
|
+
}
|
|
11088
|
+
return [key, rest];
|
|
11089
|
+
})
|
|
11090
|
+
);
|
|
11091
|
+
}
|
|
11092
|
+
const FOLDER_UNGROUPED = "__FOLDER_UNGROUPED__";
|
|
11093
|
+
function serializeComponent(component, options = {}) {
|
|
11094
|
+
const stripSet = options.includeGroupUuid ? new Set([...COMPONENT_STRIP_KEYS].filter((key) => key !== "component_group_uuid")) : COMPONENT_STRIP_KEYS;
|
|
11095
|
+
const clean = stripKeys(component, stripSet);
|
|
11096
|
+
if (clean.schema && typeof clean.schema === "object") {
|
|
11097
|
+
clean.schema = sortSchemaByPos$1(clean.schema);
|
|
11098
|
+
}
|
|
11099
|
+
const ordered = {};
|
|
11100
|
+
if (clean.name !== void 0) {
|
|
11101
|
+
ordered.name = clean.name;
|
|
11102
|
+
}
|
|
11103
|
+
if (clean.display_name !== void 0) {
|
|
11104
|
+
ordered.display_name = clean.display_name;
|
|
11105
|
+
}
|
|
11106
|
+
if (clean.is_root !== void 0) {
|
|
11107
|
+
ordered.is_root = clean.is_root;
|
|
11108
|
+
}
|
|
11109
|
+
if (clean.is_nestable !== void 0) {
|
|
11110
|
+
ordered.is_nestable = clean.is_nestable;
|
|
11111
|
+
}
|
|
11112
|
+
if ("folder" in component) {
|
|
11113
|
+
ordered.folder = component.folder === null ? FOLDER_UNGROUPED : component.folder;
|
|
11114
|
+
}
|
|
11115
|
+
const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "folder", "schema"]);
|
|
11116
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
11117
|
+
if (!handled.has(key)) {
|
|
11118
|
+
ordered[key] = value;
|
|
11119
|
+
}
|
|
11120
|
+
}
|
|
11121
|
+
if (clean.schema !== void 0) {
|
|
11122
|
+
ordered.schema = clean.schema;
|
|
11123
|
+
}
|
|
11124
|
+
return `defineBlock(${formatValue(ordered, 0)})`.replace(`folder: '${FOLDER_UNGROUPED}'`, "folder: null");
|
|
11125
|
+
}
|
|
11126
|
+
function serializeDatasource(datasource) {
|
|
11127
|
+
const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
|
|
11128
|
+
if (Array.isArray(clean.dimensions)) {
|
|
11129
|
+
clean.dimensions = clean.dimensions.map(
|
|
11130
|
+
(dim) => typeof dim === "object" && dim !== null ? stripKeys(dim, DATASOURCE_DIMENSION_STRIP_KEYS) : dim
|
|
11131
|
+
);
|
|
11132
|
+
}
|
|
11133
|
+
const ordered = {};
|
|
11134
|
+
if (clean.name !== void 0) {
|
|
11135
|
+
ordered.name = clean.name;
|
|
11136
|
+
}
|
|
11137
|
+
if (clean.slug !== void 0) {
|
|
11138
|
+
ordered.slug = clean.slug;
|
|
11139
|
+
}
|
|
11140
|
+
const handled = /* @__PURE__ */ new Set(["name", "slug"]);
|
|
11141
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
11142
|
+
if (!handled.has(key)) {
|
|
11143
|
+
ordered[key] = value;
|
|
11144
|
+
}
|
|
11145
|
+
}
|
|
11146
|
+
return `defineDatasource(${formatValue(ordered, 0)})`;
|
|
11147
|
+
}
|
|
11148
|
+
|
|
11149
|
+
function translateGroupWhitelist(schema, uuidToPath) {
|
|
11150
|
+
if (!isRecord(schema)) {
|
|
11151
|
+
return schema;
|
|
11152
|
+
}
|
|
11153
|
+
const result = {};
|
|
11154
|
+
for (const [fieldName, field] of Object.entries(schema)) {
|
|
11155
|
+
if (isRecord(field) && Array.isArray(field.component_group_whitelist)) {
|
|
11156
|
+
result[fieldName] = {
|
|
11157
|
+
...field,
|
|
11158
|
+
component_group_whitelist: field.component_group_whitelist.map((entry) => typeof entry === "string" ? uuidToPath.get(entry) ?? entry : entry)
|
|
11159
|
+
};
|
|
11160
|
+
} else {
|
|
11161
|
+
result[fieldName] = field;
|
|
11162
|
+
}
|
|
11163
|
+
}
|
|
11164
|
+
return result;
|
|
11165
|
+
}
|
|
11166
|
+
function diffEntity(type, name, localSerialized, remoteSerialized) {
|
|
11167
|
+
if (!remoteSerialized && localSerialized) {
|
|
11168
|
+
return { type, name, action: "create", diff: null, local: null, remote: null };
|
|
11169
|
+
}
|
|
11170
|
+
if (remoteSerialized && !localSerialized) {
|
|
11171
|
+
return { type, name, action: "stale", diff: null, local: null, remote: null };
|
|
11172
|
+
}
|
|
11173
|
+
if (localSerialized === remoteSerialized) {
|
|
11174
|
+
return { type, name, action: "unchanged", diff: null, local: null, remote: null };
|
|
11175
|
+
}
|
|
11176
|
+
const patch = createTwoFilesPatch(
|
|
11177
|
+
`remote/${name}`,
|
|
11178
|
+
`local/${name}`,
|
|
11179
|
+
remoteSerialized,
|
|
11180
|
+
localSerialized,
|
|
11181
|
+
"remote",
|
|
11182
|
+
"local"
|
|
11183
|
+
);
|
|
11184
|
+
return { type, name, action: "update", diff: patch, local: null, remote: null };
|
|
11185
|
+
}
|
|
11186
|
+
function diffSchema(local, remote) {
|
|
11187
|
+
const diffs = [];
|
|
11188
|
+
const groupPathByUuid = buildGroupPathByUuid([...remote.componentFolders.values()]);
|
|
11189
|
+
const uuidToPath = /* @__PURE__ */ new Map();
|
|
11190
|
+
for (const [uuid, segments] of groupPathByUuid) {
|
|
11191
|
+
uuidToPath.set(uuid, segments.join("/"));
|
|
11192
|
+
}
|
|
11193
|
+
const remoteFolderPaths = new Set(uuidToPath.values());
|
|
11194
|
+
const localFolderPaths = new Set(local.folders.map((f) => f.path));
|
|
11195
|
+
for (const folder of local.folders) {
|
|
11196
|
+
const action = remoteFolderPaths.has(folder.path) ? "unchanged" : "create";
|
|
11197
|
+
diffs.push({ type: "folder", name: folder.path, action, diff: null, local: null, remote: null });
|
|
11198
|
+
}
|
|
11199
|
+
for (const path of remoteFolderPaths) {
|
|
11200
|
+
if (!localFolderPaths.has(path)) {
|
|
11201
|
+
diffs.push({ type: "folder", name: path, action: "stale", diff: null, local: null, remote: null });
|
|
11202
|
+
}
|
|
11203
|
+
}
|
|
11204
|
+
const processedComponentNames = /* @__PURE__ */ new Set();
|
|
11205
|
+
for (const comp of local.components) {
|
|
11206
|
+
processedComponentNames.add(comp.name);
|
|
11207
|
+
const remoteComp = remote.components.get(comp.name);
|
|
11208
|
+
const includeGroupUuid = typeof comp.component_group_uuid === "string";
|
|
11209
|
+
const localForDiff = { ...comp };
|
|
11210
|
+
const remoteForDiff = remoteComp ? { ...remoteComp } : void 0;
|
|
11211
|
+
if ("folder" in comp) {
|
|
11212
|
+
if (remoteForDiff) {
|
|
11213
|
+
const uuid = remoteForDiff.component_group_uuid;
|
|
11214
|
+
remoteForDiff.folder = typeof uuid === "string" && uuid ? uuidToPath.get(uuid) ?? null : null;
|
|
11215
|
+
}
|
|
11216
|
+
} else {
|
|
11217
|
+
delete localForDiff.folder;
|
|
11218
|
+
if (remoteForDiff) {
|
|
11219
|
+
delete remoteForDiff.folder;
|
|
11220
|
+
}
|
|
11221
|
+
}
|
|
11222
|
+
localForDiff.schema = translateGroupWhitelist(localForDiff.schema, uuidToPath);
|
|
11223
|
+
if (remoteForDiff) {
|
|
11224
|
+
remoteForDiff.schema = translateGroupWhitelist(remoteForDiff.schema, uuidToPath);
|
|
11225
|
+
}
|
|
11226
|
+
const localSerialized = serializeComponent(applyDefaults(localForDiff, COMPONENT_DEFAULTS), { includeGroupUuid });
|
|
11227
|
+
const remoteSerialized = remoteForDiff ? serializeComponent(applyDefaults(remoteForDiff, COMPONENT_DEFAULTS), { includeGroupUuid }) : null;
|
|
11228
|
+
diffs.push(diffEntity("component", comp.name, localSerialized, remoteSerialized));
|
|
11229
|
+
}
|
|
11230
|
+
for (const [name] of remote.components) {
|
|
11231
|
+
if (!processedComponentNames.has(name)) {
|
|
11232
|
+
diffs.push(diffEntity("component", name, null, "stale"));
|
|
11233
|
+
}
|
|
11234
|
+
}
|
|
11235
|
+
const processedDatasourceNames = /* @__PURE__ */ new Set();
|
|
11236
|
+
for (const ds of local.datasources) {
|
|
11237
|
+
processedDatasourceNames.add(ds.name);
|
|
11238
|
+
const remoteDs = remote.datasources.get(ds.name);
|
|
11239
|
+
const localSerialized = serializeDatasource(applyDefaults(ds, DATASOURCE_DEFAULTS));
|
|
11240
|
+
const remoteSerialized = remoteDs ? serializeDatasource(applyDefaults(remoteDs, DATASOURCE_DEFAULTS)) : null;
|
|
11241
|
+
diffs.push(diffEntity("datasource", ds.name, localSerialized, remoteSerialized));
|
|
11242
|
+
}
|
|
11243
|
+
for (const [name] of remote.datasources) {
|
|
11244
|
+
if (!processedDatasourceNames.has(name)) {
|
|
11245
|
+
diffs.push(diffEntity("datasource", name, null, "stale"));
|
|
11246
|
+
}
|
|
11247
|
+
}
|
|
11248
|
+
return {
|
|
11249
|
+
diffs,
|
|
11250
|
+
creates: diffs.filter((d) => d.action === "create").length,
|
|
11251
|
+
updates: diffs.filter((d) => d.action === "update").length,
|
|
11252
|
+
unchanged: diffs.filter((d) => d.action === "unchanged").length,
|
|
11253
|
+
stale: diffs.filter((d) => d.action === "stale").length
|
|
11254
|
+
};
|
|
11255
|
+
}
|
|
11256
|
+
|
|
11257
|
+
async function fetchRemoteSchema(spaceId) {
|
|
11258
|
+
const client = getMapiClient();
|
|
11259
|
+
const spaceIdNum = Number(spaceId);
|
|
11260
|
+
const [componentsRes, foldersRes, rawDatasources] = await Promise.all([
|
|
11261
|
+
client.components.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
|
|
11262
|
+
client.componentFolders.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
|
|
11263
|
+
fetchAllPages(
|
|
11264
|
+
(page) => client.datasources.list({ path: { space_id: spaceIdNum }, query: { page }, throwOnError: true }),
|
|
11265
|
+
(data) => data?.datasources ?? []
|
|
11266
|
+
)
|
|
11267
|
+
]);
|
|
11268
|
+
const rawComponents = componentsRes.data?.components ?? [];
|
|
11269
|
+
const rawComponentFolders = foldersRes.data?.component_groups ?? [];
|
|
11270
|
+
const remote = {
|
|
11271
|
+
components: new Map(rawComponents.map((c) => [c.name, c])),
|
|
11272
|
+
componentFolders: new Map(rawComponentFolders.map((f) => [f.name, f])),
|
|
11273
|
+
datasources: new Map(rawDatasources.map((d) => [d.name, d]))
|
|
11274
|
+
};
|
|
11275
|
+
return { remote, rawComponents, rawComponentFolders, rawDatasources };
|
|
11276
|
+
}
|
|
11277
|
+
|
|
11278
|
+
function isSchemaField(value) {
|
|
11279
|
+
return isRecord(value) && "type" in value;
|
|
11280
|
+
}
|
|
11281
|
+
function toSchemaRecord(schema) {
|
|
11282
|
+
const result = {};
|
|
11283
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
11284
|
+
if (key === "_uid" || key === "component" || !isSchemaField(value)) {
|
|
11285
|
+
continue;
|
|
11286
|
+
}
|
|
11287
|
+
result[key] = value;
|
|
11288
|
+
}
|
|
11289
|
+
return result;
|
|
11290
|
+
}
|
|
11291
|
+
function buildComponentPayload(input) {
|
|
11292
|
+
if (!isRecord(input)) {
|
|
11293
|
+
return { name: "" };
|
|
11294
|
+
}
|
|
11295
|
+
return {
|
|
11296
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
11297
|
+
// Fields in COMPONENT_DEFAULTS are always sent with their reset value so that
|
|
11298
|
+
// removing a field from the local schema actually clears it on the API.
|
|
11299
|
+
// (Root-level fields are additive on MAPI update — omitting preserves the old value.)
|
|
11300
|
+
display_name: typeof input.display_name === "string" ? input.display_name : "",
|
|
11301
|
+
description: typeof input.description === "string" ? input.description : "",
|
|
11302
|
+
color: typeof input.color === "string" ? input.color : "",
|
|
11303
|
+
icon: typeof input.icon === "string" ? input.icon : "",
|
|
11304
|
+
preview_field: typeof input.preview_field === "string" ? input.preview_field : "",
|
|
11305
|
+
internal_tag_ids: Array.isArray(input.internal_tag_ids) ? input.internal_tag_ids : [],
|
|
11306
|
+
// Conditionally sent: only included when explicitly set in local schema
|
|
11307
|
+
...isRecord(input.schema) && { schema: toSchemaRecord(input.schema) },
|
|
11308
|
+
...typeof input.is_root === "boolean" && { is_root: input.is_root },
|
|
11309
|
+
...typeof input.is_nestable === "boolean" && { is_nestable: input.is_nestable },
|
|
11310
|
+
// Forward the group membership only when the key is present on input: a
|
|
11311
|
+
// string sets the group, an explicit `null` clears it. Unmanaged components
|
|
11312
|
+
// (key absent) omit it so their remote group is left untouched.
|
|
11313
|
+
..."component_group_uuid" in input && (typeof input.component_group_uuid === "string" || input.component_group_uuid === null) && { component_group_uuid: input.component_group_uuid }
|
|
11314
|
+
};
|
|
11315
|
+
}
|
|
11316
|
+
function toComponentCreate(input) {
|
|
11317
|
+
return buildComponentPayload(input);
|
|
11318
|
+
}
|
|
11319
|
+
function toComponentUpdate(input) {
|
|
11320
|
+
return buildComponentPayload(input);
|
|
11321
|
+
}
|
|
11322
|
+
function toDatasourceCreate(input) {
|
|
11323
|
+
if (!isRecord(input)) {
|
|
11324
|
+
return { name: "", slug: "" };
|
|
11325
|
+
}
|
|
11326
|
+
const result = {
|
|
11327
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
11328
|
+
slug: typeof input.slug === "string" ? input.slug : ""
|
|
11329
|
+
};
|
|
11330
|
+
if (Array.isArray(input.dimensions)) {
|
|
11331
|
+
result.dimensions_attributes = input.dimensions.filter((d) => isRecord(d) && typeof d.name === "string" && typeof d.entry_value === "string").map((d) => ({
|
|
11332
|
+
name: d.name,
|
|
11333
|
+
entry_value: d.entry_value
|
|
11334
|
+
}));
|
|
11335
|
+
}
|
|
11336
|
+
return result;
|
|
11337
|
+
}
|
|
11338
|
+
function toDatasourceUpdate(input, remote) {
|
|
11339
|
+
const base = toDatasourceCreate(input);
|
|
11340
|
+
const localDims = base.dimensions_attributes ?? [];
|
|
11341
|
+
const remoteDims = remote.dimensions ?? [];
|
|
11342
|
+
if (remoteDims.length === 0) {
|
|
11343
|
+
return base;
|
|
11344
|
+
}
|
|
11345
|
+
const localKeys = new Set(localDims.map((d) => `${d.name}::${d.entry_value}`));
|
|
11346
|
+
const destroyEntries = remoteDims.filter((rd) => rd.id != null && !localKeys.has(`${rd.name}::${rd.entry_value}`)).map((rd) => ({ id: rd.id, _destroy: true }));
|
|
11347
|
+
if (destroyEntries.length > 0) {
|
|
11348
|
+
return {
|
|
11349
|
+
...base,
|
|
11350
|
+
dimensions_attributes: [...localDims, ...destroyEntries]
|
|
11351
|
+
};
|
|
11352
|
+
}
|
|
11353
|
+
return base;
|
|
11354
|
+
}
|
|
11355
|
+
|
|
11356
|
+
function resolveGroupRefs(comp, groupByPath) {
|
|
11357
|
+
const { folder, ...rest } = comp;
|
|
11358
|
+
const resolved = { ...rest };
|
|
11359
|
+
if (typeof folder === "string") {
|
|
11360
|
+
const group = groupByPath.get(folder);
|
|
11361
|
+
if (!group) {
|
|
11362
|
+
throw new CommandError(
|
|
11363
|
+
`Unknown folder path "${folder}" for component "${comp.name}": no matching component group exists. Folder creation runs before component push, so this indicates an internal inconsistency.`
|
|
11364
|
+
);
|
|
11365
|
+
}
|
|
11366
|
+
resolved.component_group_uuid = group.uuid;
|
|
11367
|
+
} else if (folder === null) {
|
|
11368
|
+
resolved.component_group_uuid = null;
|
|
11369
|
+
}
|
|
11370
|
+
if (isRecord(resolved.schema)) {
|
|
11371
|
+
const schema = {};
|
|
11372
|
+
for (const [key, field] of Object.entries(resolved.schema)) {
|
|
11373
|
+
if (isRecord(field) && Array.isArray(field.component_group_whitelist)) {
|
|
11374
|
+
schema[key] = {
|
|
11375
|
+
...field,
|
|
11376
|
+
component_group_whitelist: field.component_group_whitelist.map((path) => typeof path === "string" ? groupByPath.get(path)?.uuid ?? path : path)
|
|
11377
|
+
};
|
|
11378
|
+
} else {
|
|
11379
|
+
schema[key] = field;
|
|
11380
|
+
}
|
|
11381
|
+
}
|
|
11382
|
+
resolved.schema = schema;
|
|
11383
|
+
}
|
|
11384
|
+
return resolved;
|
|
11385
|
+
}
|
|
11386
|
+
function buildGroupByPath(remote) {
|
|
11387
|
+
const remoteFolders = [...remote.componentFolders.values()];
|
|
11388
|
+
const groupPathByUuid = buildGroupPathByUuid(remoteFolders);
|
|
11389
|
+
const groupByPath = /* @__PURE__ */ new Map();
|
|
11390
|
+
for (const folder of remoteFolders) {
|
|
11391
|
+
const segments = groupPathByUuid.get(folder.uuid);
|
|
11392
|
+
if (segments?.length) {
|
|
11393
|
+
groupByPath.set(segments.join("/"), { id: folder.id, uuid: folder.uuid });
|
|
11394
|
+
}
|
|
11395
|
+
}
|
|
11396
|
+
return groupByPath;
|
|
11397
|
+
}
|
|
11398
|
+
function formatDiffOutput(result, options) {
|
|
11399
|
+
const lines = [];
|
|
11400
|
+
const byType = {
|
|
11401
|
+
component: [],
|
|
11402
|
+
datasource: [],
|
|
11403
|
+
folder: []
|
|
11404
|
+
};
|
|
11405
|
+
for (const diff of result.diffs) {
|
|
11406
|
+
byType[diff.type].push(diff);
|
|
11407
|
+
}
|
|
11408
|
+
const willDelete = options?.delete ?? false;
|
|
11409
|
+
const icons = {
|
|
11410
|
+
create: chalk.green("+"),
|
|
11411
|
+
update: chalk.yellow("~"),
|
|
11412
|
+
unchanged: chalk.dim("="),
|
|
11413
|
+
stale: chalk.red("-")
|
|
11414
|
+
};
|
|
11415
|
+
const sections = [
|
|
11416
|
+
["Folders", byType.folder],
|
|
11417
|
+
["Components", byType.component],
|
|
11418
|
+
["Datasources", byType.datasource]
|
|
11419
|
+
];
|
|
11420
|
+
for (const [label, diffs] of sections) {
|
|
11421
|
+
if (diffs.length === 0) {
|
|
11422
|
+
continue;
|
|
11423
|
+
}
|
|
11424
|
+
lines.push(chalk.bold(label));
|
|
11425
|
+
for (const diff of diffs) {
|
|
11426
|
+
const icon = icons[diff.action] ?? " ";
|
|
11427
|
+
const name = diff.action === "stale" ? chalk.red(diff.name) : diff.name;
|
|
11428
|
+
const actionLabel = diff.action === "stale" && willDelete ? "delete" : diff.action;
|
|
11429
|
+
lines.push(` ${icon} ${name} ${chalk.dim(`(${actionLabel})`)}`);
|
|
11430
|
+
if (diff.diff) {
|
|
11431
|
+
for (const line of diff.diff.split("\n")) {
|
|
11432
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
11433
|
+
lines.push(` ${chalk.green(line)}`);
|
|
11434
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
11435
|
+
lines.push(` ${chalk.red(line)}`);
|
|
11436
|
+
}
|
|
11437
|
+
}
|
|
11438
|
+
}
|
|
11439
|
+
}
|
|
11440
|
+
lines.push("");
|
|
11441
|
+
}
|
|
11442
|
+
const summary = [
|
|
11443
|
+
result.creates > 0 ? chalk.green(`${result.creates} to create`) : null,
|
|
11444
|
+
result.updates > 0 ? chalk.yellow(`${result.updates} to update`) : null,
|
|
11445
|
+
result.unchanged > 0 ? chalk.dim(`${result.unchanged} unchanged`) : null,
|
|
11446
|
+
result.stale > 0 ? chalk.red(`${result.stale} ${willDelete ? "to delete" : "stale"}`) : null
|
|
11447
|
+
].filter(Boolean).join(", ");
|
|
11448
|
+
lines.push(`Summary: ${summary}`);
|
|
11449
|
+
return lines.join("\n");
|
|
11450
|
+
}
|
|
11451
|
+
async function executePush(spaceId, local, remote, diffResult, options) {
|
|
11452
|
+
const client = getMapiClient();
|
|
11453
|
+
const spaceIdNum = Number(spaceId);
|
|
11454
|
+
let created = 0;
|
|
11455
|
+
let updated = 0;
|
|
11456
|
+
let deleted = 0;
|
|
11457
|
+
const groupByPath = buildGroupByPath(remote);
|
|
11458
|
+
const foldersByPath = new Map(local.folders.map((f) => [f.path, f]));
|
|
11459
|
+
const folderCreates = diffResult.diffs.filter((d) => d.type === "folder" && d.action === "create").sort((a, b) => a.name.split("/").length - b.name.split("/").length);
|
|
11460
|
+
for (const diff of folderCreates) {
|
|
11461
|
+
const localFolder = foldersByPath.get(diff.name);
|
|
11462
|
+
if (!localFolder) {
|
|
11463
|
+
continue;
|
|
11464
|
+
}
|
|
11465
|
+
const parent = localFolder.parentPath ? groupByPath.get(localFolder.parentPath) : void 0;
|
|
11466
|
+
let createdGroup;
|
|
11467
|
+
try {
|
|
11468
|
+
const res = await client.componentFolders.create({
|
|
11469
|
+
path: { space_id: spaceIdNum },
|
|
11470
|
+
body: { component_group: { name: localFolder.name, parent_id: parent?.id ?? null } },
|
|
11471
|
+
throwOnError: true
|
|
11472
|
+
});
|
|
11473
|
+
createdGroup = res.data?.component_group;
|
|
11474
|
+
} catch (error) {
|
|
11475
|
+
handleAPIError("push_component_folder", error, `Failed to create folder ${localFolder.name}`);
|
|
11476
|
+
}
|
|
11477
|
+
if (createdGroup?.id == null || !createdGroup.uuid) {
|
|
11478
|
+
throw new CommandError(
|
|
11479
|
+
`Folder "${localFolder.name}" was created but the Management API response did not include an id and uuid; blocks that reference this folder cannot be resolved.`
|
|
11480
|
+
);
|
|
11481
|
+
}
|
|
11482
|
+
groupByPath.set(localFolder.path, { id: createdGroup.id, uuid: createdGroup.uuid });
|
|
11483
|
+
created++;
|
|
11484
|
+
}
|
|
11485
|
+
const componentDiffs = diffResult.diffs.filter((d) => d.type === "component");
|
|
11486
|
+
const resolvedComponents = /* @__PURE__ */ new Map();
|
|
11487
|
+
for (const diff of componentDiffs) {
|
|
11488
|
+
if (diff.action !== "create" && diff.action !== "update") {
|
|
11489
|
+
continue;
|
|
11490
|
+
}
|
|
11491
|
+
const localComp = local.components.find((c) => c.name === diff.name);
|
|
11492
|
+
if (localComp) {
|
|
11493
|
+
resolvedComponents.set(diff.name, resolveGroupRefs(localComp, groupByPath));
|
|
11494
|
+
}
|
|
11495
|
+
}
|
|
11496
|
+
const componentResults = await Promise.allSettled(
|
|
11497
|
+
componentDiffs.map(async (diff) => {
|
|
11498
|
+
const resolvedComp = resolvedComponents.get(diff.name);
|
|
11499
|
+
if (diff.action === "create" && resolvedComp) {
|
|
11500
|
+
await client.components.create({
|
|
11501
|
+
path: { space_id: spaceIdNum },
|
|
11502
|
+
body: { component: toComponentCreate(resolvedComp) },
|
|
11503
|
+
throwOnError: true
|
|
11504
|
+
});
|
|
11505
|
+
return "created";
|
|
11506
|
+
}
|
|
11507
|
+
if (diff.action === "update" && resolvedComp) {
|
|
11508
|
+
const existing = remote.components.get(diff.name);
|
|
11509
|
+
if (existing?.id) {
|
|
11510
|
+
await client.components.update(existing.id, {
|
|
11511
|
+
path: { space_id: spaceIdNum },
|
|
11512
|
+
body: { component: toComponentUpdate(resolvedComp) },
|
|
11513
|
+
throwOnError: true
|
|
11514
|
+
});
|
|
11515
|
+
return "updated";
|
|
11516
|
+
}
|
|
11517
|
+
}
|
|
11518
|
+
})
|
|
11519
|
+
);
|
|
11520
|
+
for (let i = 0; i < componentResults.length; i++) {
|
|
11521
|
+
const result = componentResults[i];
|
|
11522
|
+
const diff = componentDiffs[i];
|
|
11523
|
+
if (result.status === "fulfilled") {
|
|
11524
|
+
if (result.value === "created") {
|
|
11525
|
+
created++;
|
|
11526
|
+
} else if (result.value === "updated") {
|
|
11527
|
+
updated++;
|
|
11528
|
+
}
|
|
11529
|
+
} else {
|
|
11530
|
+
const eventId = diff.action === "create" ? "push_component" : "update_component";
|
|
11531
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} component ${diff.name}`);
|
|
11532
|
+
}
|
|
11533
|
+
}
|
|
11534
|
+
const datasourceDiffs = diffResult.diffs.filter((d) => d.type === "datasource");
|
|
11535
|
+
const datasourceResults = await Promise.allSettled(
|
|
11536
|
+
datasourceDiffs.map(async (diff) => {
|
|
11537
|
+
const localDs = local.datasources.find((d) => d.name === diff.name);
|
|
11538
|
+
if (diff.action === "create" && localDs) {
|
|
11539
|
+
await client.datasources.create({
|
|
11540
|
+
path: { space_id: spaceIdNum },
|
|
11541
|
+
body: { datasource: toDatasourceCreate(localDs) },
|
|
11542
|
+
throwOnError: true
|
|
11543
|
+
});
|
|
11544
|
+
return "created";
|
|
11545
|
+
}
|
|
11546
|
+
if (diff.action === "update" && localDs) {
|
|
11547
|
+
const existing = remote.datasources.get(diff.name);
|
|
11548
|
+
if (existing?.id) {
|
|
11549
|
+
await client.datasources.update(existing.id, {
|
|
11550
|
+
path: { space_id: spaceIdNum },
|
|
11551
|
+
body: { datasource: toDatasourceUpdate(localDs, existing) },
|
|
11552
|
+
throwOnError: true
|
|
11553
|
+
});
|
|
11554
|
+
return "updated";
|
|
11555
|
+
}
|
|
11556
|
+
}
|
|
11557
|
+
})
|
|
11558
|
+
);
|
|
11559
|
+
for (let i = 0; i < datasourceResults.length; i++) {
|
|
11560
|
+
const result = datasourceResults[i];
|
|
11561
|
+
const diff = datasourceDiffs[i];
|
|
11562
|
+
if (result.status === "fulfilled") {
|
|
11563
|
+
if (result.value === "created") {
|
|
11564
|
+
created++;
|
|
11565
|
+
} else if (result.value === "updated") {
|
|
11566
|
+
updated++;
|
|
11567
|
+
}
|
|
11568
|
+
} else {
|
|
11569
|
+
const eventId = diff.action === "create" ? "push_datasource" : "update_datasource";
|
|
11570
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} datasource ${diff.name}`);
|
|
11571
|
+
}
|
|
11572
|
+
}
|
|
11573
|
+
if (options.delete) {
|
|
11574
|
+
const deleteErrors = [];
|
|
11575
|
+
const staleComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
|
|
11576
|
+
const deleteComponentResults = await Promise.allSettled(
|
|
11577
|
+
staleComponents.map(async (diff) => {
|
|
11578
|
+
const existing = remote.components.get(diff.name);
|
|
11579
|
+
if (existing?.id) {
|
|
11580
|
+
await client.components.delete(existing.id, {
|
|
11581
|
+
path: { space_id: spaceIdNum },
|
|
11582
|
+
throwOnError: true
|
|
11583
|
+
});
|
|
11584
|
+
return true;
|
|
11585
|
+
}
|
|
11586
|
+
})
|
|
11587
|
+
);
|
|
11588
|
+
for (let i = 0; i < deleteComponentResults.length; i++) {
|
|
11589
|
+
const result = deleteComponentResults[i];
|
|
11590
|
+
if (result.status === "fulfilled") {
|
|
11591
|
+
if (result.value) {
|
|
11592
|
+
deleted++;
|
|
11593
|
+
}
|
|
11594
|
+
} else {
|
|
11595
|
+
deleteErrors.push({ action: "delete_component", reason: result.reason, message: `Failed to delete component ${staleComponents[i].name}` });
|
|
11596
|
+
}
|
|
11597
|
+
}
|
|
11598
|
+
const staleDatasources = diffResult.diffs.filter((d) => d.type === "datasource" && d.action === "stale");
|
|
11599
|
+
const deleteDatasourceResults = await Promise.allSettled(
|
|
11600
|
+
staleDatasources.map(async (diff) => {
|
|
11601
|
+
const existing = remote.datasources.get(diff.name);
|
|
11602
|
+
if (existing?.id) {
|
|
11603
|
+
await client.datasources.delete(existing.id, {
|
|
11604
|
+
path: { space_id: spaceIdNum },
|
|
11605
|
+
throwOnError: true
|
|
11606
|
+
});
|
|
11607
|
+
return true;
|
|
11608
|
+
}
|
|
11609
|
+
})
|
|
11610
|
+
);
|
|
11611
|
+
for (let i = 0; i < deleteDatasourceResults.length; i++) {
|
|
11612
|
+
const result = deleteDatasourceResults[i];
|
|
11613
|
+
if (result.status === "fulfilled") {
|
|
11614
|
+
if (result.value) {
|
|
11615
|
+
deleted++;
|
|
11616
|
+
}
|
|
11617
|
+
} else {
|
|
11618
|
+
deleteErrors.push({ action: "delete_datasource", reason: result.reason, message: `Failed to delete datasource ${staleDatasources[i].name}` });
|
|
11619
|
+
}
|
|
11620
|
+
}
|
|
11621
|
+
const staleFolders = diffResult.diffs.filter((d) => d.type === "folder" && d.action === "stale").sort((a, b) => b.name.split("/").length - a.name.split("/").length);
|
|
11622
|
+
for (const diff of staleFolders) {
|
|
11623
|
+
const group = groupByPath.get(diff.name);
|
|
11624
|
+
if (!group) {
|
|
11625
|
+
deleteErrors.push({
|
|
11626
|
+
action: "delete_component_folder",
|
|
11627
|
+
reason: new Error(`No component group resolved for stale folder path "${diff.name}".`),
|
|
11628
|
+
message: `Failed to delete folder ${diff.name}`
|
|
11629
|
+
});
|
|
11630
|
+
continue;
|
|
11631
|
+
}
|
|
11632
|
+
try {
|
|
11633
|
+
await client.componentFolders.delete(group.id, {
|
|
11634
|
+
path: { space_id: spaceIdNum },
|
|
11635
|
+
throwOnError: true
|
|
11636
|
+
});
|
|
11637
|
+
deleted++;
|
|
11638
|
+
} catch (error) {
|
|
11639
|
+
deleteErrors.push({ action: "delete_component_folder", reason: error, message: `Failed to delete folder ${diff.name}` });
|
|
11640
|
+
}
|
|
11641
|
+
}
|
|
11642
|
+
if (deleteErrors.length > 0) {
|
|
11643
|
+
const first = deleteErrors[0];
|
|
11644
|
+
const suffix = deleteErrors.length > 1 ? ` (and ${deleteErrors.length - 1} more delete error(s))` : "";
|
|
11645
|
+
handleAPIError(first.action, first.reason, `${first.message}${suffix}`);
|
|
11646
|
+
}
|
|
11647
|
+
}
|
|
11648
|
+
return { created, updated, deleted };
|
|
11649
|
+
}
|
|
11650
|
+
function buildChangesetEntries(diffResult, local, remote, options) {
|
|
11651
|
+
const changes = [];
|
|
11652
|
+
const remoteFolders = [...remote.componentFolders.values()];
|
|
11653
|
+
const folderPathByUuid = buildGroupPathByUuid(remoteFolders);
|
|
11654
|
+
const remoteFolderByPath = /* @__PURE__ */ new Map();
|
|
11655
|
+
for (const folder of remoteFolders) {
|
|
11656
|
+
const segments = folderPathByUuid.get(folder.uuid);
|
|
11657
|
+
if (segments?.length) {
|
|
11658
|
+
remoteFolderByPath.set(segments.join("/"), folder);
|
|
11659
|
+
}
|
|
11660
|
+
}
|
|
11661
|
+
for (const diff of diffResult.diffs) {
|
|
11662
|
+
if (diff.action === "unchanged") {
|
|
11663
|
+
continue;
|
|
11664
|
+
}
|
|
11665
|
+
if (diff.action === "stale" && !options.delete) {
|
|
11666
|
+
continue;
|
|
11667
|
+
}
|
|
11668
|
+
const action = diff.action === "stale" ? "delete" : diff.action;
|
|
11669
|
+
let remoteSrc;
|
|
11670
|
+
let localSrc;
|
|
11671
|
+
if (diff.type === "component") {
|
|
11672
|
+
remoteSrc = remote.components.get(diff.name);
|
|
11673
|
+
localSrc = local.components.find((c) => c.name === diff.name);
|
|
11674
|
+
} else if (diff.type === "datasource") {
|
|
11675
|
+
remoteSrc = remote.datasources.get(diff.name);
|
|
11676
|
+
localSrc = local.datasources.find((d) => d.name === diff.name);
|
|
11677
|
+
} else if (diff.type === "folder") {
|
|
11678
|
+
remoteSrc = remoteFolderByPath.get(diff.name);
|
|
11679
|
+
localSrc = local.folders.find((f) => f.path === diff.name);
|
|
11680
|
+
}
|
|
11681
|
+
changes.push({
|
|
11682
|
+
type: diff.type,
|
|
11683
|
+
name: diff.name,
|
|
11684
|
+
action,
|
|
11685
|
+
...remoteSrc && { before: { ...remoteSrc } },
|
|
11686
|
+
...localSrc && { after: { ...localSrc } }
|
|
11687
|
+
});
|
|
11688
|
+
}
|
|
11689
|
+
return changes;
|
|
11690
|
+
}
|
|
11691
|
+
|
|
11692
|
+
async function ensureDir(dir) {
|
|
11693
|
+
await mkdir(dir, { recursive: true });
|
|
11694
|
+
}
|
|
11695
|
+
async function saveChangeset(basePath, data) {
|
|
11696
|
+
const dir = join(basePath, "schema", "changesets");
|
|
11697
|
+
await ensureDir(dir);
|
|
11698
|
+
const fileName = `${fileTimestamp(data.timestamp)}.json`;
|
|
11699
|
+
const filePath = join(dir, fileName);
|
|
11700
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
11701
|
+
return filePath;
|
|
11702
|
+
}
|
|
11703
|
+
|
|
11704
|
+
const SENTINEL_FIELDS = /* @__PURE__ */ new Set(["_uid", "component"]);
|
|
11705
|
+
function classifyFieldChanges(remoteSchema, localSchema) {
|
|
11706
|
+
const removed = [];
|
|
11707
|
+
const added = [];
|
|
11708
|
+
const typeChanged = [];
|
|
11709
|
+
const requiredAdded = [];
|
|
11710
|
+
const requiredChanged = [];
|
|
11711
|
+
for (const [field, remoteField] of Object.entries(remoteSchema)) {
|
|
11712
|
+
if (SENTINEL_FIELDS.has(field)) {
|
|
11713
|
+
continue;
|
|
11714
|
+
}
|
|
11715
|
+
if (typeof remoteField.type !== "string") {
|
|
11716
|
+
continue;
|
|
11717
|
+
}
|
|
11718
|
+
if (!(field in localSchema)) {
|
|
11719
|
+
removed.push({ field, type: remoteField.type });
|
|
11720
|
+
}
|
|
11721
|
+
}
|
|
11722
|
+
for (const [field, localField] of Object.entries(localSchema)) {
|
|
11723
|
+
if (SENTINEL_FIELDS.has(field)) {
|
|
11724
|
+
continue;
|
|
11725
|
+
}
|
|
11726
|
+
if (typeof localField.type !== "string") {
|
|
11727
|
+
continue;
|
|
11728
|
+
}
|
|
11729
|
+
if (!(field in remoteSchema)) {
|
|
11730
|
+
if (localField.required) {
|
|
11731
|
+
requiredAdded.push({ field, type: localField.type });
|
|
11732
|
+
} else {
|
|
11733
|
+
added.push({ field, type: localField.type, required: false });
|
|
11734
|
+
}
|
|
11735
|
+
} else {
|
|
11736
|
+
const remoteField = remoteSchema[field];
|
|
11737
|
+
if (typeof remoteField?.type !== "string") {
|
|
11738
|
+
continue;
|
|
11739
|
+
}
|
|
11740
|
+
if (remoteField.type !== localField.type) {
|
|
11741
|
+
typeChanged.push({ field, oldType: remoteField.type, newType: localField.type });
|
|
11742
|
+
}
|
|
11743
|
+
if (localField.required && !remoteField.required) {
|
|
11744
|
+
requiredChanged.push({ field, type: localField.type });
|
|
11745
|
+
}
|
|
11746
|
+
}
|
|
11747
|
+
}
|
|
11748
|
+
return { removed, added, typeChanged, requiredAdded, requiredChanged };
|
|
11749
|
+
}
|
|
11750
|
+
function longestCommonSubstring(a, b) {
|
|
11751
|
+
let maxLen = 0;
|
|
11752
|
+
for (let i = 0; i < a.length; i++) {
|
|
11753
|
+
for (let j = 0; j < b.length; j++) {
|
|
11754
|
+
let len = 0;
|
|
11755
|
+
while (i + len < a.length && j + len < b.length && a[i + len] === b[j + len]) {
|
|
11756
|
+
len++;
|
|
11757
|
+
}
|
|
11758
|
+
if (len > maxLen) {
|
|
11759
|
+
maxLen = len;
|
|
11760
|
+
}
|
|
11761
|
+
}
|
|
11762
|
+
}
|
|
11763
|
+
return maxLen;
|
|
11764
|
+
}
|
|
11765
|
+
function nameSimilarity(a, b) {
|
|
11766
|
+
const longer = Math.max(a.length, b.length);
|
|
11767
|
+
if (longer === 0) {
|
|
11768
|
+
return 1;
|
|
11769
|
+
}
|
|
11770
|
+
return longestCommonSubstring(a, b) / longer;
|
|
11771
|
+
}
|
|
11772
|
+
function detectRenames(removed, added) {
|
|
11773
|
+
const renames = [];
|
|
11774
|
+
const usedRemoved = /* @__PURE__ */ new Set();
|
|
11775
|
+
const usedAdded = /* @__PURE__ */ new Set();
|
|
11776
|
+
const addedByType = /* @__PURE__ */ new Map();
|
|
11777
|
+
for (const addedField of added) {
|
|
11778
|
+
if (!addedByType.has(addedField.type)) {
|
|
11779
|
+
addedByType.set(addedField.type, []);
|
|
11780
|
+
}
|
|
11781
|
+
addedByType.get(addedField.type).push(addedField);
|
|
11782
|
+
}
|
|
11783
|
+
const isSinglePair = removed.length === 1 && added.length === 1;
|
|
11784
|
+
for (const removedField of removed) {
|
|
11785
|
+
const candidates = addedByType.get(removedField.type) ?? [];
|
|
11786
|
+
const availableCandidates = candidates.filter((c) => !usedAdded.has(c.field));
|
|
11787
|
+
if (availableCandidates.length === 0) {
|
|
11788
|
+
continue;
|
|
11789
|
+
}
|
|
11790
|
+
let bestCandidate = availableCandidates[0];
|
|
11791
|
+
let bestScore = nameSimilarity(removedField.field, bestCandidate.field);
|
|
11792
|
+
for (let i = 1; i < availableCandidates.length; i++) {
|
|
11793
|
+
const score = nameSimilarity(removedField.field, availableCandidates[i].field);
|
|
11794
|
+
if (score > bestScore) {
|
|
11795
|
+
bestScore = score;
|
|
11796
|
+
bestCandidate = availableCandidates[i];
|
|
11797
|
+
}
|
|
11798
|
+
}
|
|
11799
|
+
if (!isSinglePair && bestScore < 0.3) {
|
|
11800
|
+
continue;
|
|
11801
|
+
}
|
|
11802
|
+
renames.push({ oldField: removedField.field, newField: bestCandidate.field, fieldType: removedField.type });
|
|
11803
|
+
usedRemoved.add(removedField.field);
|
|
11804
|
+
usedAdded.add(bestCandidate.field);
|
|
11805
|
+
}
|
|
11806
|
+
const unmatchedRemoved = removed.filter((r) => !usedRemoved.has(r.field));
|
|
11807
|
+
const unmatchedAdded = added.filter((a) => !usedAdded.has(a.field));
|
|
11808
|
+
return { renames, unmatchedRemoved, unmatchedAdded };
|
|
11809
|
+
}
|
|
11810
|
+
function analyzeBreakingChanges(diffResult, local, remote) {
|
|
11811
|
+
const results = [];
|
|
11812
|
+
const updatedComponents = diffResult.diffs.filter(
|
|
11813
|
+
(d) => d.type === "component" && d.action === "update"
|
|
11814
|
+
);
|
|
11815
|
+
for (const diff of updatedComponents) {
|
|
11816
|
+
const localComp = local.components.find((c) => c.name === diff.name);
|
|
11817
|
+
const remoteComp = remote.components.get(diff.name);
|
|
11818
|
+
if (!localComp?.schema || !remoteComp?.schema) {
|
|
11819
|
+
continue;
|
|
11820
|
+
}
|
|
11821
|
+
const classification = classifyFieldChanges(
|
|
11822
|
+
remoteComp.schema,
|
|
11823
|
+
localComp.schema
|
|
11824
|
+
);
|
|
11825
|
+
const changes = [];
|
|
11826
|
+
const { renames, unmatchedRemoved } = detectRenames(classification.removed, classification.added);
|
|
11827
|
+
for (const rename of renames) {
|
|
11828
|
+
changes.push({ kind: "rename", field: rename.newField, oldField: rename.oldField });
|
|
11829
|
+
}
|
|
11830
|
+
for (const removed of unmatchedRemoved) {
|
|
11831
|
+
changes.push({ kind: "removed", field: removed.field });
|
|
11832
|
+
}
|
|
11833
|
+
for (const tc of classification.typeChanged) {
|
|
11834
|
+
changes.push({ kind: "type_changed", field: tc.field, oldType: tc.oldType, newType: tc.newType });
|
|
11835
|
+
}
|
|
11836
|
+
for (const ra of classification.requiredAdded) {
|
|
11837
|
+
changes.push({ kind: "required_added", field: ra.field, fieldType: ra.type });
|
|
11838
|
+
}
|
|
11839
|
+
for (const rc of classification.requiredChanged) {
|
|
11840
|
+
changes.push({ kind: "required_changed", field: rc.field, fieldType: rc.type });
|
|
11841
|
+
}
|
|
11842
|
+
if (changes.length > 0) {
|
|
11843
|
+
results.push({ componentName: diff.name, changes });
|
|
11844
|
+
}
|
|
11845
|
+
}
|
|
11846
|
+
return results;
|
|
11847
|
+
}
|
|
11848
|
+
|
|
11849
|
+
const COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["text:textarea", "textarea:text"]);
|
|
11850
|
+
function defaultForType(fieldType) {
|
|
11851
|
+
switch (fieldType) {
|
|
11852
|
+
case "text":
|
|
11853
|
+
case "textarea":
|
|
11854
|
+
case "markdown":
|
|
11855
|
+
return `''`;
|
|
11856
|
+
case "number":
|
|
11857
|
+
return "0";
|
|
11858
|
+
case "boolean":
|
|
11859
|
+
return "false";
|
|
11860
|
+
default:
|
|
11861
|
+
return null;
|
|
11862
|
+
}
|
|
11863
|
+
}
|
|
11864
|
+
function typeConversion(field, oldType, newType) {
|
|
11865
|
+
const key = `${oldType}:${newType}`;
|
|
11866
|
+
if (COMPATIBLE_TYPES.has(key)) {
|
|
11867
|
+
return null;
|
|
11868
|
+
}
|
|
11869
|
+
const accessor = `block.${field}`;
|
|
11870
|
+
switch (key) {
|
|
11871
|
+
case "text:number":
|
|
11872
|
+
return `${accessor} = Number(${accessor}) || 0;`;
|
|
11873
|
+
case "number:text":
|
|
11874
|
+
return `${accessor} = String(${accessor});`;
|
|
11875
|
+
case "text:boolean":
|
|
11876
|
+
return `${accessor} = !!${accessor};`;
|
|
11877
|
+
case "boolean:text":
|
|
11878
|
+
return `${accessor} = String(${accessor});`;
|
|
11879
|
+
default:
|
|
11880
|
+
return `${accessor}; // TODO: convert from ${oldType} to ${newType}`;
|
|
11881
|
+
}
|
|
11882
|
+
}
|
|
11883
|
+
function renderMigrationCode(changes) {
|
|
11884
|
+
const lines = [];
|
|
11885
|
+
lines.push(" // Review this migration before running it against your space.");
|
|
11886
|
+
lines.push(" // Generated migrations are scaffolds and may need manual adjustments.");
|
|
11887
|
+
lines.push(" // Example rename migration:");
|
|
11888
|
+
lines.push(" // block.new_field = block.old_field;");
|
|
11889
|
+
lines.push(" // delete block.old_field;");
|
|
11890
|
+
lines.push("");
|
|
11891
|
+
for (const change of changes) {
|
|
11892
|
+
switch (change.kind) {
|
|
11893
|
+
case "rename":
|
|
11894
|
+
lines.push(` // Rename: ${change.oldField} \u2192 ${change.field}`);
|
|
11895
|
+
lines.push(` if ('${change.oldField}' in block) {`);
|
|
11896
|
+
lines.push(` block.${change.field} = block.${change.oldField};`);
|
|
11897
|
+
lines.push(` delete block.${change.oldField};`);
|
|
11898
|
+
lines.push(` }`);
|
|
11899
|
+
break;
|
|
11900
|
+
case "removed":
|
|
11901
|
+
if (change.renameHint) {
|
|
11902
|
+
lines.push(` // If '${change.field}' was renamed to '${change.renameHint.newField}', uncomment:`);
|
|
11903
|
+
lines.push(` // block.${change.renameHint.newField} = block.${change.field};`);
|
|
11904
|
+
} else {
|
|
11905
|
+
lines.push(` // Removed field: ${change.field}`);
|
|
11906
|
+
}
|
|
11907
|
+
lines.push(` delete block.${change.field};`);
|
|
11908
|
+
break;
|
|
11909
|
+
case "type_changed": {
|
|
11910
|
+
const conversion = typeConversion(change.field, change.oldType, change.newType);
|
|
11911
|
+
if (conversion) {
|
|
11912
|
+
lines.push(` // Type change: ${change.field} (${change.oldType} \u2192 ${change.newType})`);
|
|
11913
|
+
lines.push(` ${conversion}`);
|
|
11914
|
+
}
|
|
11915
|
+
break;
|
|
11916
|
+
}
|
|
11917
|
+
case "required_added": {
|
|
11918
|
+
const defaultValue = defaultForType(change.fieldType);
|
|
11919
|
+
lines.push(` // New required field: ${change.field} (${change.fieldType})`);
|
|
11920
|
+
if (defaultValue !== null) {
|
|
11921
|
+
lines.push(` // TODO: provide a meaningful default value`);
|
|
11922
|
+
lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
|
|
11923
|
+
} else {
|
|
11924
|
+
lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
|
|
11925
|
+
lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
|
|
11926
|
+
}
|
|
11927
|
+
break;
|
|
11928
|
+
}
|
|
11929
|
+
case "required_changed": {
|
|
11930
|
+
const defaultValue = defaultForType(change.fieldType);
|
|
11931
|
+
lines.push(` // Field is now required: ${change.field} (${change.fieldType})`);
|
|
11932
|
+
lines.push(` // Existing stories may have null/undefined values \u2014 provide a default for those.`);
|
|
11933
|
+
if (defaultValue !== null) {
|
|
11934
|
+
lines.push(` // TODO: provide a meaningful default value`);
|
|
11935
|
+
lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
|
|
11936
|
+
} else {
|
|
11937
|
+
lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
|
|
11938
|
+
lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
|
|
11939
|
+
}
|
|
11940
|
+
break;
|
|
11941
|
+
}
|
|
11942
|
+
}
|
|
11943
|
+
lines.push("");
|
|
11944
|
+
}
|
|
11945
|
+
const body = lines.length > 0 ? `
|
|
11946
|
+
${lines.join("\n")}` : "\n";
|
|
11947
|
+
return `export default function (block) {${body} return block;
|
|
11948
|
+
}
|
|
11949
|
+
`;
|
|
11950
|
+
}
|
|
11951
|
+
async function writeMigrationFile(options) {
|
|
11952
|
+
const { spaceId, componentName, code, timestamp, basePath } = options;
|
|
11953
|
+
const dir = resolvePath(basePath, `migrations/${spaceId}`);
|
|
11954
|
+
await mkdir(dir, { recursive: true });
|
|
11955
|
+
const fileName = `${componentName}.${fileTimestamp(timestamp)}.js`;
|
|
11956
|
+
const filePath = join(dir, fileName);
|
|
11957
|
+
await writeFile(filePath, code, "utf-8");
|
|
11958
|
+
return filePath;
|
|
11959
|
+
}
|
|
11960
|
+
|
|
11961
|
+
const DEFAULT_GROUPS_FILENAME = "groups.json";
|
|
11962
|
+
const CONSOLIDATED_COMPONENTS_FILENAME = "components.json";
|
|
11963
|
+
function sanitizeForLocalWrite(component) {
|
|
11964
|
+
const { folder, ...rest } = component;
|
|
11965
|
+
if (isRecord(rest.schema)) {
|
|
11966
|
+
const schema = {};
|
|
11967
|
+
for (const [key, field] of Object.entries(rest.schema)) {
|
|
11968
|
+
if (isRecord(field) && "component_group_whitelist" in field) {
|
|
11969
|
+
const { component_group_whitelist, restrict_components, ...fieldRest } = field;
|
|
11970
|
+
if (fieldRest.restrict_type === "groups") {
|
|
11971
|
+
delete fieldRest.restrict_type;
|
|
11972
|
+
}
|
|
11973
|
+
schema[key] = fieldRest;
|
|
11974
|
+
} else {
|
|
11975
|
+
schema[key] = field;
|
|
11976
|
+
}
|
|
11977
|
+
}
|
|
11978
|
+
rest.schema = schema;
|
|
11979
|
+
}
|
|
11980
|
+
return rest;
|
|
11981
|
+
}
|
|
11982
|
+
async function writeLocalComponents({
|
|
11983
|
+
space,
|
|
11984
|
+
basePath,
|
|
11985
|
+
resolved,
|
|
11986
|
+
diffResult,
|
|
11987
|
+
deleteRemoved,
|
|
11988
|
+
ui,
|
|
11989
|
+
logger
|
|
11990
|
+
}) {
|
|
11991
|
+
const componentsDir = resolveCommandPath(directories.components, space, basePath);
|
|
11992
|
+
const consolidatedPath = join(componentsDir, CONSOLIDATED_COMPONENTS_FILENAME);
|
|
11993
|
+
if (await fileExists(consolidatedPath)) {
|
|
11994
|
+
ui.warn(
|
|
11995
|
+
`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.`
|
|
11996
|
+
);
|
|
11997
|
+
}
|
|
11998
|
+
for (const component of resolved.components) {
|
|
11999
|
+
const filePath = join(componentsDir, `${sanitizeFilename(component.name || "")}.json`);
|
|
12000
|
+
await saveToFile(filePath, JSON.stringify(sanitizeForLocalWrite(component), null, 2));
|
|
12001
|
+
}
|
|
12002
|
+
const groupsPath = join(componentsDir, DEFAULT_GROUPS_FILENAME);
|
|
12003
|
+
if (await fileExists(groupsPath)) {
|
|
12004
|
+
try {
|
|
12005
|
+
await unlink(groupsPath);
|
|
12006
|
+
logger.info("Removed stale local groups file", { path: displayPath(groupsPath, basePath) });
|
|
12007
|
+
} catch (error) {
|
|
12008
|
+
if (error.code !== "ENOENT") {
|
|
12009
|
+
throw error;
|
|
12010
|
+
}
|
|
12011
|
+
}
|
|
12012
|
+
}
|
|
12013
|
+
if (deleteRemoved) {
|
|
12014
|
+
const staleComponents = diffResult.diffs.filter(
|
|
12015
|
+
(d) => d.type === "component" && d.action === "stale"
|
|
12016
|
+
);
|
|
12017
|
+
for (const stale of staleComponents) {
|
|
12018
|
+
const filePath = join(componentsDir, `${sanitizeFilename(stale.name)}.json`);
|
|
12019
|
+
try {
|
|
12020
|
+
await unlink(filePath);
|
|
12021
|
+
logger.info("Removed stale local component file", { path: displayPath(filePath, basePath) });
|
|
12022
|
+
} catch (error) {
|
|
12023
|
+
if (error.code !== "ENOENT") {
|
|
12024
|
+
throw error;
|
|
12025
|
+
}
|
|
12026
|
+
}
|
|
12027
|
+
}
|
|
12028
|
+
}
|
|
12029
|
+
logger.info("Wrote local component files", {
|
|
12030
|
+
space,
|
|
12031
|
+
componentsWritten: resolved.components.length
|
|
12032
|
+
});
|
|
12033
|
+
}
|
|
12034
|
+
|
|
12035
|
+
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) => {
|
|
12036
|
+
const ui = getUI();
|
|
12037
|
+
const logger = getLogger();
|
|
12038
|
+
const reporter = getReporter();
|
|
12039
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
12040
|
+
const { state } = session();
|
|
12041
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Pushing schema...");
|
|
12042
|
+
logger.info("Schema push started", { entryFile, space });
|
|
12043
|
+
if (!requireAuthentication(state, verbose)) {
|
|
12044
|
+
return;
|
|
12045
|
+
}
|
|
12046
|
+
if (!space) {
|
|
12047
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
12048
|
+
return;
|
|
12049
|
+
}
|
|
12050
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
12051
|
+
try {
|
|
12052
|
+
const loadSpinner = ui.createSpinner("Resolving schema...");
|
|
12053
|
+
let local;
|
|
12054
|
+
try {
|
|
12055
|
+
local = await loadSchema(entryFile);
|
|
12056
|
+
} catch (maybeError) {
|
|
12057
|
+
loadSpinner.failed("Failed to resolve schema");
|
|
12058
|
+
handleError(toError(maybeError), verbose);
|
|
12059
|
+
return;
|
|
12060
|
+
}
|
|
12061
|
+
loadSpinner.succeed(`Found: ${local.components.length} components, ${local.folders.length} folders, ${local.datasources.length} datasources`);
|
|
12062
|
+
const totalLocal = local.components.length + local.datasources.length + local.folders.length;
|
|
12063
|
+
if (totalLocal === 0) {
|
|
12064
|
+
ui.warn("No components, folders, or datasources found in the entry file. Verify the file exports schema definitions.");
|
|
12065
|
+
return;
|
|
12066
|
+
}
|
|
12067
|
+
const remoteSpinner = ui.createSpinner(`Fetching remote state from space ${space}...`);
|
|
12068
|
+
let remoteResult;
|
|
12069
|
+
try {
|
|
12070
|
+
remoteResult = await fetchRemoteSchema(space);
|
|
12071
|
+
} catch (maybeError) {
|
|
12072
|
+
remoteSpinner.failed("Failed to fetch remote schema");
|
|
12073
|
+
handleError(toError(maybeError), verbose);
|
|
12074
|
+
return;
|
|
12075
|
+
}
|
|
12076
|
+
const { remote, rawComponents, rawComponentFolders, rawDatasources } = remoteResult;
|
|
12077
|
+
remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.datasources.size} datasources`);
|
|
12078
|
+
const diffResult = diffSchema(local, remote);
|
|
12079
|
+
ui.br();
|
|
12080
|
+
ui.log(formatDiffOutput(diffResult, { delete: options.delete }));
|
|
12081
|
+
if (options.migrations) {
|
|
12082
|
+
const breakingChanges = analyzeBreakingChanges(diffResult, local, remote);
|
|
12083
|
+
if (breakingChanges.length > 0) {
|
|
12084
|
+
const totalChanges = breakingChanges.reduce((sum, c) => sum + c.changes.length, 0);
|
|
12085
|
+
ui.br();
|
|
12086
|
+
ui.warn(`${totalChanges} breaking change(s) detected in ${breakingChanges.length} component(s).`);
|
|
12087
|
+
ui.info("Generated migrations are scaffolds. Review and adjust them before running `storyblok migrations run`.");
|
|
12088
|
+
if (!options.dryRun) {
|
|
12089
|
+
const explicitMigrations = command.getOptionValueSource("migrations") === "cli";
|
|
12090
|
+
const shouldGenerate = explicitMigrations || await confirm({
|
|
12091
|
+
message: "Generate migration files for breaking changes?",
|
|
12092
|
+
default: true
|
|
12093
|
+
});
|
|
12094
|
+
if (shouldGenerate) {
|
|
12095
|
+
const migrationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
12096
|
+
const resolvedBase = resolvePath(basePath, "");
|
|
12097
|
+
for (const comp of breakingChanges) {
|
|
12098
|
+
const renames = comp.changes.filter((c) => c.kind === "rename");
|
|
12099
|
+
if (renames.length > 0 && explicitMigrations) {
|
|
12100
|
+
for (const r of renames) {
|
|
12101
|
+
if (r.kind === "rename") {
|
|
12102
|
+
ui.log(` Assumed rename in '${comp.componentName}': ${r.oldField} \u2192 ${r.field}`);
|
|
12103
|
+
}
|
|
12104
|
+
}
|
|
12105
|
+
}
|
|
12106
|
+
if (renames.length > 0 && !explicitMigrations) {
|
|
12107
|
+
ui.br();
|
|
12108
|
+
ui.log(`Detected renames in '${comp.componentName}':`);
|
|
12109
|
+
for (const r of renames) {
|
|
12110
|
+
if (r.kind === "rename") {
|
|
12111
|
+
ui.log(` ${r.oldField} \u2192 ${r.field}`);
|
|
12112
|
+
}
|
|
12113
|
+
}
|
|
12114
|
+
const renameConfirmed = await confirm({
|
|
12115
|
+
message: "Are these renames correct?",
|
|
12116
|
+
default: true
|
|
12117
|
+
});
|
|
12118
|
+
if (!renameConfirmed) {
|
|
12119
|
+
comp.changes = comp.changes.map((c) => {
|
|
12120
|
+
if (c.kind === "rename") {
|
|
12121
|
+
return { kind: "removed", field: c.oldField, renameHint: { newField: c.field } };
|
|
12122
|
+
}
|
|
12123
|
+
return c;
|
|
12124
|
+
});
|
|
12125
|
+
}
|
|
12126
|
+
}
|
|
12127
|
+
const code = renderMigrationCode(comp.changes);
|
|
12128
|
+
const path = await writeMigrationFile({
|
|
12129
|
+
spaceId: space,
|
|
12130
|
+
componentName: comp.componentName,
|
|
12131
|
+
code,
|
|
12132
|
+
timestamp: migrationTimestamp,
|
|
12133
|
+
basePath: resolvedBase
|
|
12134
|
+
});
|
|
12135
|
+
const migrationPath = displayPath(path, basePath);
|
|
12136
|
+
logger.info("Migration generated", { component: comp.componentName, path: migrationPath });
|
|
12137
|
+
ui.log(` Generated: ${migrationPath}`);
|
|
12138
|
+
}
|
|
12139
|
+
ui.br();
|
|
12140
|
+
ui.info(`Run migrations when ready: storyblok migrations run --space ${space}`);
|
|
12141
|
+
}
|
|
12142
|
+
}
|
|
12143
|
+
}
|
|
12144
|
+
}
|
|
12145
|
+
if (options.delete) {
|
|
12146
|
+
const deletedComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
|
|
12147
|
+
for (const comp of deletedComponents) {
|
|
12148
|
+
ui.warn(`Component '${comp.name}' will be deleted. Stories using it will have out-of-schema content.`);
|
|
12149
|
+
}
|
|
12150
|
+
const staleFolders = diffResult.diffs.filter((d) => d.type === "folder" && d.action === "stale");
|
|
12151
|
+
if (staleFolders.length > 0) {
|
|
12152
|
+
const uuidByPath = /* @__PURE__ */ new Map();
|
|
12153
|
+
for (const [uuid, segments] of buildGroupPathByUuid([...remote.componentFolders.values()])) {
|
|
12154
|
+
uuidByPath.set(segments.join("/"), uuid);
|
|
12155
|
+
}
|
|
12156
|
+
const staleComponentNames = new Set(
|
|
12157
|
+
diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale").map((d) => d.name)
|
|
12158
|
+
);
|
|
12159
|
+
for (const folder of staleFolders) {
|
|
12160
|
+
const uuid = uuidByPath.get(folder.name);
|
|
12161
|
+
if (!uuid) {
|
|
12162
|
+
continue;
|
|
12163
|
+
}
|
|
12164
|
+
const count = [...remote.components.values()].filter((c) => c.component_group_uuid === uuid && !staleComponentNames.has(c.name)).length;
|
|
12165
|
+
if (count > 0) {
|
|
12166
|
+
ui.warn(`Folder '${folder.name}' will be deleted; ${count} component(s) inside will be ungrouped.`);
|
|
12167
|
+
}
|
|
12168
|
+
}
|
|
12169
|
+
}
|
|
12170
|
+
}
|
|
12171
|
+
if (diffResult.stale > 0 && !options.delete) {
|
|
12172
|
+
ui.warn(`${diffResult.stale} stale entity(s) exist remotely but not in schema. Use --delete to remove.`);
|
|
12173
|
+
}
|
|
12174
|
+
if (options.dryRun) {
|
|
12175
|
+
ui.info("Dry run \u2014 no changes applied.");
|
|
12176
|
+
logger.info("Dry run completed", { creates: diffResult.creates, updates: diffResult.updates });
|
|
12177
|
+
return;
|
|
12178
|
+
}
|
|
12179
|
+
const resolvedPath = resolvePath(basePath, "");
|
|
12180
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
12181
|
+
const changesetPath = await saveChangeset(resolvedPath, {
|
|
12182
|
+
timestamp,
|
|
12183
|
+
spaceId: Number(space),
|
|
12184
|
+
remote: { components: rawComponents, componentFolders: rawComponentFolders, datasources: rawDatasources },
|
|
12185
|
+
changes: buildChangesetEntries(diffResult, local, remote, { delete: options.delete })
|
|
12186
|
+
});
|
|
12187
|
+
logger.info("Changeset saved", { path: displayPath(changesetPath, basePath) });
|
|
12188
|
+
const nothingToPush = diffResult.creates === 0 && diffResult.updates === 0 && (!options.delete || diffResult.stale === 0);
|
|
12189
|
+
if (nothingToPush) {
|
|
12190
|
+
ui.ok("Everything up to date \u2014 nothing to push.");
|
|
12191
|
+
} else {
|
|
12192
|
+
const pushSpinner = ui.createSpinner("Pushing schema...");
|
|
12193
|
+
let result;
|
|
12194
|
+
try {
|
|
12195
|
+
result = await executePush(space, local, remote, diffResult, { delete: options.delete });
|
|
12196
|
+
} catch (error) {
|
|
12197
|
+
pushSpinner.failed("Failed to push schema");
|
|
12198
|
+
throw error;
|
|
12199
|
+
}
|
|
12200
|
+
summary.total = result.created + result.updated + result.deleted;
|
|
12201
|
+
summary.succeeded = summary.total;
|
|
12202
|
+
pushSpinner.succeed(`Pushed ${result.created} creations, ${result.updated} updates${result.deleted > 0 ? `, ${result.deleted} deletions` : ""}.`);
|
|
12203
|
+
}
|
|
12204
|
+
if (options.writeComponents) {
|
|
12205
|
+
try {
|
|
12206
|
+
await writeLocalComponents({
|
|
12207
|
+
space,
|
|
12208
|
+
basePath,
|
|
12209
|
+
resolved: local,
|
|
12210
|
+
diffResult,
|
|
12211
|
+
deleteRemoved: options.delete,
|
|
12212
|
+
ui,
|
|
12213
|
+
logger
|
|
12214
|
+
});
|
|
12215
|
+
} catch (writeError) {
|
|
12216
|
+
ui.warn(`Failed to write local component files: ${toError(writeError).message}`);
|
|
12217
|
+
logger.warn("Failed to write local component files", { error: toError(writeError).message });
|
|
12218
|
+
}
|
|
12219
|
+
}
|
|
12220
|
+
} catch (maybeError) {
|
|
12221
|
+
summary.failed += 1;
|
|
12222
|
+
handleError(toError(maybeError), verbose);
|
|
12223
|
+
} finally {
|
|
12224
|
+
logger.info("Schema push finished", { summary });
|
|
12225
|
+
reporter.addSummary("schemaPushResults", summary);
|
|
12226
|
+
reporter.finalize();
|
|
12227
|
+
}
|
|
12228
|
+
});
|
|
12229
|
+
|
|
12230
|
+
const FIELD_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "pos"]);
|
|
12231
|
+
function toCamelCaseIdentifier(str) {
|
|
12232
|
+
const camel = slugify(str).replace(/^[_-]+/, "").replace(/[_-]+(.)/g, (_, char) => char.toUpperCase());
|
|
12233
|
+
if (!camel) {
|
|
12234
|
+
return "_";
|
|
12235
|
+
}
|
|
12236
|
+
return /^\d/.test(camel) ? `_${camel}` : camel;
|
|
12237
|
+
}
|
|
12238
|
+
function toKebabCase(str) {
|
|
12239
|
+
return str.replace(/[\s_]+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
12240
|
+
}
|
|
12241
|
+
function componentVarName(name) {
|
|
12242
|
+
return `${toCamelCaseIdentifier(name)}Block`;
|
|
12243
|
+
}
|
|
12244
|
+
function datasourceVarName(name) {
|
|
12245
|
+
return `${toCamelCaseIdentifier(name)}Datasource`;
|
|
12246
|
+
}
|
|
12247
|
+
function folderVarName(name) {
|
|
12248
|
+
return `${toCamelCaseIdentifier(name)}Folder`;
|
|
12249
|
+
}
|
|
12250
|
+
function resolveVarNames(rawNames, baseVarName) {
|
|
12251
|
+
const used = /* @__PURE__ */ new Set();
|
|
12252
|
+
return rawNames.map((raw) => {
|
|
12253
|
+
const base = baseVarName(raw);
|
|
12254
|
+
let candidate = base;
|
|
12255
|
+
let n = 2;
|
|
12256
|
+
while (used.has(candidate)) {
|
|
12257
|
+
candidate = `${base}${n++}`;
|
|
12258
|
+
}
|
|
12259
|
+
used.add(candidate);
|
|
12260
|
+
return candidate;
|
|
12261
|
+
});
|
|
12262
|
+
}
|
|
12263
|
+
function resolveFileNames(baseNames, dirKeys) {
|
|
12264
|
+
const usedByDir = /* @__PURE__ */ new Map();
|
|
12265
|
+
return baseNames.map((base, i) => {
|
|
12266
|
+
const dir = dirKeys?.[i] ?? "";
|
|
12267
|
+
let used = usedByDir.get(dir);
|
|
12268
|
+
if (!used) {
|
|
12269
|
+
used = /* @__PURE__ */ new Set();
|
|
12270
|
+
usedByDir.set(dir, used);
|
|
12271
|
+
}
|
|
12272
|
+
let candidate = base;
|
|
12273
|
+
let n = 2;
|
|
12274
|
+
while (used.has(candidate)) {
|
|
12275
|
+
candidate = `${base}-${n++}`;
|
|
12276
|
+
}
|
|
12277
|
+
used.add(candidate);
|
|
12278
|
+
return candidate;
|
|
12279
|
+
});
|
|
12280
|
+
}
|
|
12281
|
+
function componentFileName(name) {
|
|
12282
|
+
return toKebabCase(name);
|
|
12283
|
+
}
|
|
12284
|
+
function datasourceFileName(datasource) {
|
|
12285
|
+
return toKebabCase(datasource.slug || datasource.name);
|
|
12286
|
+
}
|
|
12287
|
+
function resolveComponents(components, segmentsByIndex) {
|
|
12288
|
+
const varNames = resolveVarNames(components.map((c) => c.name), componentVarName);
|
|
12289
|
+
const fileNames = resolveFileNames(
|
|
12290
|
+
components.map((c) => componentFileName(c.name)),
|
|
12291
|
+
segmentsByIndex.map((segments) => segments.join("/"))
|
|
12292
|
+
);
|
|
12293
|
+
return components.map((component, i) => ({
|
|
12294
|
+
component,
|
|
12295
|
+
varName: varNames[i],
|
|
12296
|
+
fileName: fileNames[i],
|
|
12297
|
+
segments: segmentsByIndex[i]
|
|
12298
|
+
}));
|
|
12299
|
+
}
|
|
12300
|
+
function resolveDatasources(datasources) {
|
|
12301
|
+
const varNames = resolveVarNames(datasources.map((d) => d.name), datasourceVarName);
|
|
12302
|
+
const fileNames = resolveFileNames(datasources.map((d) => datasourceFileName(d)));
|
|
12303
|
+
return datasources.map((datasource, i) => ({
|
|
12304
|
+
datasource,
|
|
12305
|
+
varName: varNames[i],
|
|
12306
|
+
fileName: fileNames[i]
|
|
12307
|
+
}));
|
|
12308
|
+
}
|
|
12309
|
+
function resolveFolders(folders) {
|
|
12310
|
+
const pathByUuid = buildGroupPathByUuid(folders);
|
|
12311
|
+
const ordered = [...folders].sort((a, b) => (pathByUuid.get(a.uuid)?.length ?? 0) - (pathByUuid.get(b.uuid)?.length ?? 0));
|
|
12312
|
+
const varNames = resolveVarNames(ordered.map((f) => f.name), folderVarName);
|
|
12313
|
+
return ordered.map((folder, i) => ({
|
|
12314
|
+
folder,
|
|
12315
|
+
varName: varNames[i],
|
|
12316
|
+
segments: pathByUuid.get(folder.uuid) ?? []
|
|
12317
|
+
}));
|
|
12318
|
+
}
|
|
12319
|
+
function resolveGroupWhitelistRefs(whitelist, folderVarByUuid) {
|
|
12320
|
+
if (!folderVarByUuid || !Array.isArray(whitelist) || whitelist.length === 0) {
|
|
12321
|
+
return void 0;
|
|
12322
|
+
}
|
|
12323
|
+
const vars = whitelist.map((uuid) => typeof uuid === "string" ? folderVarByUuid.get(uuid) : void 0);
|
|
12324
|
+
if (!vars.every((v) => typeof v === "string")) {
|
|
12325
|
+
return void 0;
|
|
12326
|
+
}
|
|
12327
|
+
return vars.map((v) => new RawCode(v));
|
|
12328
|
+
}
|
|
12329
|
+
function toDslField(field, folderVarByUuid) {
|
|
12330
|
+
const { component_whitelist, component_group_whitelist, datasource_slug, restrict_components, restrict_type, ...rest } = field;
|
|
12331
|
+
const out = { ...rest };
|
|
12332
|
+
const groupRefs = resolveGroupWhitelistRefs(component_group_whitelist, folderVarByUuid);
|
|
12333
|
+
const hasBlockNames = Array.isArray(component_whitelist) && component_whitelist.length > 0;
|
|
12334
|
+
if (hasBlockNames) {
|
|
12335
|
+
out.allow = component_whitelist;
|
|
12336
|
+
} else if (groupRefs) {
|
|
12337
|
+
out.allow = groupRefs;
|
|
12338
|
+
} else if (component_group_whitelist !== void 0) {
|
|
12339
|
+
out.component_group_whitelist = component_group_whitelist;
|
|
12340
|
+
if (restrict_components !== void 0) {
|
|
12341
|
+
out.restrict_components = restrict_components;
|
|
12342
|
+
}
|
|
12343
|
+
if (restrict_type !== void 0) {
|
|
12344
|
+
out.restrict_type = restrict_type;
|
|
12345
|
+
}
|
|
12346
|
+
}
|
|
12347
|
+
if (datasource_slug !== void 0) {
|
|
12348
|
+
out.datasource = datasource_slug;
|
|
12349
|
+
}
|
|
12350
|
+
return out;
|
|
12351
|
+
}
|
|
12352
|
+
function omitEmptyArrays(obj) {
|
|
12353
|
+
const out = {};
|
|
12354
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
12355
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
12356
|
+
continue;
|
|
12357
|
+
}
|
|
12358
|
+
out[key] = value;
|
|
12359
|
+
}
|
|
12360
|
+
return out;
|
|
12361
|
+
}
|
|
12362
|
+
function generateFieldCode(fieldName, fieldData, depth, folderVarByUuid) {
|
|
12363
|
+
const clean = omitEmptyArrays(toDslField(stripKeys(fieldData, FIELD_STRIP_KEYS), folderVarByUuid));
|
|
12364
|
+
return `defineField(${quoteString(fieldName)}, ${formatValue(clean, depth)})`;
|
|
12365
|
+
}
|
|
12366
|
+
function collectWhitelistFolderVars(schema, folderVarByUuid) {
|
|
12367
|
+
const vars = /* @__PURE__ */ new Set();
|
|
12368
|
+
for (const field of Object.values(schema)) {
|
|
12369
|
+
if (!isRecord(field)) {
|
|
12370
|
+
continue;
|
|
12371
|
+
}
|
|
12372
|
+
const refs = resolveGroupWhitelistRefs(field.component_group_whitelist, folderVarByUuid);
|
|
12373
|
+
if (refs) {
|
|
12374
|
+
refs.forEach((ref) => vars.add(ref.code));
|
|
12375
|
+
}
|
|
12376
|
+
}
|
|
12377
|
+
return [...vars].sort();
|
|
12378
|
+
}
|
|
12379
|
+
function sortSchemaByPos(schema) {
|
|
12380
|
+
return Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
|
|
12381
|
+
const posA = typeof a.pos === "number" ? a.pos : Infinity;
|
|
12382
|
+
const posB = typeof b.pos === "number" ? b.pos : Infinity;
|
|
12383
|
+
return posA - posB;
|
|
12384
|
+
});
|
|
12385
|
+
}
|
|
12386
|
+
function generateFoldersFile(resolved) {
|
|
12387
|
+
const varByUuid = new Map(resolved.map((r) => [r.folder.uuid, r.varName]));
|
|
12388
|
+
const lines = ["import { defineFolder } from '@storyblok/schema';", ""];
|
|
12389
|
+
for (const { folder, varName } of resolved) {
|
|
12390
|
+
const parentVar = folder.parent_uuid ? varByUuid.get(folder.parent_uuid) : void 0;
|
|
12391
|
+
lines.push(`export const ${varName} = defineFolder({`);
|
|
12392
|
+
lines.push(`${INDENT}name: ${quoteString(folder.name)},`);
|
|
12393
|
+
if (parentVar) {
|
|
12394
|
+
lines.push(`${INDENT}parent: ${parentVar},`);
|
|
12395
|
+
}
|
|
12396
|
+
lines.push("});");
|
|
12397
|
+
lines.push("");
|
|
12398
|
+
}
|
|
12399
|
+
return lines.join("\n");
|
|
12400
|
+
}
|
|
12401
|
+
function generateComponentFile(component, varName, folderRef, folderVarByUuid) {
|
|
12402
|
+
const lines = [];
|
|
12403
|
+
lines.push("import {");
|
|
12404
|
+
lines.push(" defineBlock,");
|
|
12405
|
+
lines.push(" defineField,");
|
|
12406
|
+
lines.push("} from '@storyblok/schema';");
|
|
12407
|
+
lines.push("");
|
|
12408
|
+
const blockDepth = folderRef?.segments.length ?? 0;
|
|
12409
|
+
const schema = isRecord(component.schema) ? component.schema : {};
|
|
12410
|
+
const folderVars = [.../* @__PURE__ */ new Set([
|
|
12411
|
+
...folderRef ? [folderRef.varName] : [],
|
|
12412
|
+
...collectWhitelistFolderVars(schema, folderVarByUuid)
|
|
12413
|
+
])].sort();
|
|
12414
|
+
if (folderVars.length > 0) {
|
|
12415
|
+
const rel = "../".repeat(blockDepth + 1);
|
|
12416
|
+
lines.push(`import { ${folderVars.join(", ")} } from '${rel}folders';`);
|
|
12417
|
+
lines.push("");
|
|
12418
|
+
}
|
|
12419
|
+
const resolvedVarName = varName ?? componentVarName(component.name);
|
|
12420
|
+
lines.push(`export const ${resolvedVarName} = defineBlock({`);
|
|
12421
|
+
const clean = omitEmptyArrays(stripKeys(component, COMPONENT_STRIP_KEYS));
|
|
12422
|
+
delete clean.component_group_uuid;
|
|
12423
|
+
const orderedKeys = [];
|
|
12424
|
+
if (clean.name !== void 0) {
|
|
12425
|
+
orderedKeys.push("name");
|
|
12426
|
+
}
|
|
12427
|
+
if (clean.display_name !== void 0) {
|
|
12428
|
+
orderedKeys.push("display_name");
|
|
12429
|
+
}
|
|
12430
|
+
if (clean.is_root !== void 0) {
|
|
12431
|
+
orderedKeys.push("is_root");
|
|
12432
|
+
}
|
|
12433
|
+
if (clean.is_nestable !== void 0) {
|
|
12434
|
+
orderedKeys.push("is_nestable");
|
|
12435
|
+
}
|
|
12436
|
+
const prefixKeyCount = orderedKeys.length;
|
|
12437
|
+
const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
|
|
12438
|
+
for (const key of Object.keys(clean).sort()) {
|
|
12439
|
+
if (!handled.has(key)) {
|
|
12440
|
+
orderedKeys.push(key);
|
|
12441
|
+
}
|
|
12442
|
+
}
|
|
12443
|
+
orderedKeys.forEach((key, i) => {
|
|
12444
|
+
lines.push(`${INDENT}${key}: ${formatValue(clean[key], 1)},`);
|
|
12445
|
+
if (folderRef && i === prefixKeyCount - 1) {
|
|
12446
|
+
lines.push(`${INDENT}folder: ${folderRef.varName},`);
|
|
12447
|
+
}
|
|
12448
|
+
});
|
|
12449
|
+
if (folderRef && prefixKeyCount === 0) {
|
|
12450
|
+
lines.push(`${INDENT}folder: ${folderRef.varName},`);
|
|
12451
|
+
}
|
|
12452
|
+
if (clean.schema && typeof clean.schema === "object") {
|
|
12453
|
+
const schema2 = clean.schema;
|
|
12454
|
+
const sortedFields = sortSchemaByPos(schema2);
|
|
12455
|
+
if (sortedFields.length > 0) {
|
|
12456
|
+
lines.push(`${INDENT}fields: [`);
|
|
12457
|
+
for (const [fieldName, fieldData] of sortedFields) {
|
|
12458
|
+
const fieldCode = generateFieldCode(fieldName, fieldData, 2, folderVarByUuid);
|
|
12459
|
+
lines.push(`${INDENT}${INDENT}${fieldCode},`);
|
|
12460
|
+
}
|
|
12461
|
+
lines.push(`${INDENT}],`);
|
|
12462
|
+
} else {
|
|
12463
|
+
lines.push(`${INDENT}fields: [],`);
|
|
12464
|
+
}
|
|
12465
|
+
}
|
|
12466
|
+
lines.push("});");
|
|
12467
|
+
lines.push("");
|
|
12468
|
+
return lines.join("\n");
|
|
12469
|
+
}
|
|
12470
|
+
function generateDatasourceFile(datasource, varName) {
|
|
12471
|
+
const lines = [];
|
|
12472
|
+
lines.push("import { defineDatasource } from '@storyblok/schema';");
|
|
12473
|
+
lines.push("");
|
|
12474
|
+
const resolvedVarName = varName ?? datasourceVarName(datasource.name);
|
|
12475
|
+
lines.push(`export const ${resolvedVarName} = defineDatasource({`);
|
|
12476
|
+
const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
|
|
12477
|
+
if (clean.name !== void 0) {
|
|
12478
|
+
lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
|
|
12479
|
+
}
|
|
12480
|
+
if (clean.slug !== void 0) {
|
|
12481
|
+
lines.push(`${INDENT}slug: ${formatValue(clean.slug, 1)},`);
|
|
12482
|
+
}
|
|
12483
|
+
const handled = /* @__PURE__ */ new Set(["name", "slug"]);
|
|
12484
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
12485
|
+
if (!handled.has(key)) {
|
|
12486
|
+
lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
|
|
12487
|
+
}
|
|
12488
|
+
}
|
|
12489
|
+
lines.push("});");
|
|
12490
|
+
lines.push("");
|
|
12491
|
+
return lines.join("\n");
|
|
12492
|
+
}
|
|
12493
|
+
function generateSchemaFile(components, datasources, folders = []) {
|
|
12494
|
+
const lines = [];
|
|
12495
|
+
lines.push("import { defineSchema } from '@storyblok/schema';");
|
|
12496
|
+
lines.push("import type { Schema as InferSchema, Story as InferStory } from '@storyblok/schema';");
|
|
12497
|
+
lines.push("import type { MapiStory as InferStoryMapi } from '@storyblok/schema';");
|
|
12498
|
+
lines.push("");
|
|
12499
|
+
for (const { varName, fileName, segments } of components) {
|
|
12500
|
+
const subPath = segments.length > 0 ? `${segments.join("/")}/` : "";
|
|
12501
|
+
lines.push(`import { ${varName} } from './blocks/${subPath}${fileName}';`);
|
|
12502
|
+
}
|
|
12503
|
+
for (const { varName, fileName } of datasources) {
|
|
12504
|
+
lines.push(`import { ${varName} } from './datasources/${fileName}';`);
|
|
12505
|
+
}
|
|
12506
|
+
if (folders.length > 0) {
|
|
12507
|
+
lines.push(`import { ${folders.map((f) => f.varName).join(", ")} } from './folders';`);
|
|
12508
|
+
}
|
|
12509
|
+
lines.push("");
|
|
12510
|
+
lines.push("export const schema = defineSchema({");
|
|
12511
|
+
if (components.length > 0) {
|
|
12512
|
+
lines.push(" blocks: {");
|
|
12513
|
+
for (const { varName } of components) {
|
|
12514
|
+
lines.push(` ${varName},`);
|
|
12515
|
+
}
|
|
12516
|
+
lines.push(" },");
|
|
12517
|
+
}
|
|
12518
|
+
if (datasources.length > 0) {
|
|
12519
|
+
lines.push(" datasources: {");
|
|
12520
|
+
for (const { varName } of datasources) {
|
|
12521
|
+
lines.push(` ${varName},`);
|
|
12522
|
+
}
|
|
12523
|
+
lines.push(" },");
|
|
12524
|
+
}
|
|
12525
|
+
if (folders.length > 0) {
|
|
12526
|
+
lines.push(" folders: {");
|
|
12527
|
+
for (const { varName } of folders) {
|
|
12528
|
+
lines.push(` ${varName},`);
|
|
12529
|
+
}
|
|
12530
|
+
lines.push(" },");
|
|
12531
|
+
}
|
|
12532
|
+
lines.push("});");
|
|
12533
|
+
lines.push("");
|
|
12534
|
+
lines.push("export type Schema = InferSchema<typeof schema>;");
|
|
12535
|
+
lines.push("export type Blocks = Schema['blocks'];");
|
|
12536
|
+
lines.push("export type Story = InferStory<Blocks>;");
|
|
12537
|
+
lines.push("export type StoryMapi = InferStoryMapi<Blocks>;");
|
|
12538
|
+
lines.push("");
|
|
12539
|
+
return lines.join("\n");
|
|
12540
|
+
}
|
|
12541
|
+
|
|
12542
|
+
async function writeFileWithDirs(filePath, content) {
|
|
12543
|
+
const dir = dirname(filePath);
|
|
12544
|
+
await mkdir(dir, { recursive: true });
|
|
12545
|
+
await writeFile(filePath, content, "utf-8");
|
|
12546
|
+
}
|
|
12547
|
+
async function writeSchemaFiles(targetPath, components, componentFolders, datasources) {
|
|
12548
|
+
const writtenFiles = [];
|
|
12549
|
+
const groupPathByUuid = buildGroupPathByUuid(componentFolders);
|
|
12550
|
+
const componentSegments = components.map(
|
|
12551
|
+
(comp) => comp.component_group_uuid ? groupPathByUuid.get(comp.component_group_uuid) ?? [] : []
|
|
12552
|
+
);
|
|
12553
|
+
const resolvedComponents = resolveComponents(components, componentSegments);
|
|
12554
|
+
const resolvedDatasources = resolveDatasources(datasources);
|
|
12555
|
+
const resolvedFolders = resolveFolders(componentFolders);
|
|
12556
|
+
const folderByUuid = new Map(resolvedFolders.map((r) => [r.folder.uuid, r]));
|
|
12557
|
+
const folderVarByUuid = new Map([...folderByUuid].map(([uuid, r]) => [uuid, r.varName]));
|
|
12558
|
+
for (const { component, varName, fileName, segments } of resolvedComponents) {
|
|
12559
|
+
const filePath = join(targetPath, "blocks", ...segments, `${fileName}.ts`);
|
|
12560
|
+
const folder = component.component_group_uuid ? folderByUuid.get(component.component_group_uuid) : void 0;
|
|
12561
|
+
const folderRef = folder && { varName: folder.varName, segments };
|
|
12562
|
+
await writeFileWithDirs(filePath, generateComponentFile(component, varName, folderRef, folderVarByUuid));
|
|
12563
|
+
writtenFiles.push(filePath);
|
|
12564
|
+
}
|
|
12565
|
+
for (const { datasource, varName, fileName } of resolvedDatasources) {
|
|
12566
|
+
const filePath = join(targetPath, "datasources", `${fileName}.ts`);
|
|
12567
|
+
await writeFileWithDirs(filePath, generateDatasourceFile(datasource, varName));
|
|
12568
|
+
writtenFiles.push(filePath);
|
|
12569
|
+
}
|
|
12570
|
+
if (resolvedFolders.length > 0) {
|
|
12571
|
+
const foldersPath = join(targetPath, "folders.ts");
|
|
12572
|
+
await writeFileWithDirs(foldersPath, generateFoldersFile(resolvedFolders));
|
|
12573
|
+
writtenFiles.push(foldersPath);
|
|
12574
|
+
}
|
|
12575
|
+
const schemaPath = join(targetPath, "schema.ts");
|
|
12576
|
+
await writeFileWithDirs(schemaPath, generateSchemaFile(resolvedComponents, resolvedDatasources, resolvedFolders));
|
|
12577
|
+
writtenFiles.push(schemaPath);
|
|
12578
|
+
return writtenFiles;
|
|
12579
|
+
}
|
|
12580
|
+
|
|
12581
|
+
async function isTargetEmpty(targetPath) {
|
|
12582
|
+
try {
|
|
12583
|
+
const entries = await readdir(targetPath);
|
|
12584
|
+
return entries.every((entry) => entry.startsWith("."));
|
|
12585
|
+
} catch (maybeError) {
|
|
12586
|
+
const error = maybeError;
|
|
12587
|
+
if (error?.code === "ENOENT") {
|
|
12588
|
+
return true;
|
|
12589
|
+
}
|
|
12590
|
+
throw error;
|
|
12591
|
+
}
|
|
12592
|
+
}
|
|
12593
|
+
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) => {
|
|
12594
|
+
const ui = getUI();
|
|
12595
|
+
const logger = getLogger();
|
|
12596
|
+
const reporter = getReporter();
|
|
12597
|
+
const { space, verbose } = command.optsWithGlobals();
|
|
12598
|
+
const { state } = session();
|
|
12599
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Initializing schema...");
|
|
12600
|
+
logger.info("Schema init started", { space });
|
|
12601
|
+
if (!requireAuthentication(state, verbose)) {
|
|
12602
|
+
return;
|
|
12603
|
+
}
|
|
12604
|
+
if (!space) {
|
|
12605
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
12606
|
+
return;
|
|
12607
|
+
}
|
|
12608
|
+
const targetPath = resolve(options.outDir);
|
|
12609
|
+
const targetDisplayPath = displayPath(targetPath, options.outDir);
|
|
12610
|
+
if (!await isTargetEmpty(targetPath)) {
|
|
12611
|
+
handleError(
|
|
12612
|
+
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.`),
|
|
12613
|
+
verbose
|
|
12614
|
+
);
|
|
12615
|
+
return;
|
|
12616
|
+
}
|
|
12617
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
12618
|
+
try {
|
|
12619
|
+
const fetchSpinner = ui.createSpinner(`Fetching schema from space ${space}...`);
|
|
12620
|
+
let fetchResult;
|
|
12621
|
+
try {
|
|
12622
|
+
fetchResult = await fetchRemoteSchema(space);
|
|
12623
|
+
} catch (maybeError) {
|
|
12624
|
+
fetchSpinner.failed("Failed to fetch remote schema");
|
|
12625
|
+
handleError(toError(maybeError), verbose);
|
|
12626
|
+
return;
|
|
12627
|
+
}
|
|
12628
|
+
const { rawComponents, rawComponentFolders, rawDatasources } = fetchResult;
|
|
12629
|
+
fetchSpinner.succeed(`Found: ${rawComponents.length} components, ${rawComponentFolders.length} component folders, ${rawDatasources.length} datasources`);
|
|
12630
|
+
const writeSpinner = ui.createSpinner(`Generating TypeScript files to ${targetDisplayPath}...`);
|
|
12631
|
+
const writtenFiles = await writeSchemaFiles(targetPath, rawComponents, rawComponentFolders, rawDatasources);
|
|
12632
|
+
summary.total = writtenFiles.length;
|
|
12633
|
+
summary.succeeded = writtenFiles.length;
|
|
12634
|
+
writeSpinner.succeed(`Generated ${writtenFiles.length} files`);
|
|
12635
|
+
ui.list(writtenFiles.map((file) => displayPath(file, options.outDir)));
|
|
12636
|
+
ui.warn("`schema init` is a one-time bootstrap step for adopting an existing space. Review generated files before continuing.");
|
|
12637
|
+
ui.info("After bootstrapping, keep your local schema as the source of truth and use `schema push` for ongoing changes.");
|
|
12638
|
+
ui.info("Make sure `@storyblok/schema` is installed in the project that imports these files (e.g. `pnpm add @storyblok/schema`).");
|
|
12639
|
+
} catch (maybeError) {
|
|
12640
|
+
summary.failed += 1;
|
|
12641
|
+
handleError(toError(maybeError), verbose);
|
|
12642
|
+
} finally {
|
|
12643
|
+
logger.info("Schema init finished", { summary });
|
|
12644
|
+
reporter.addSummary("schemaInitResults", summary);
|
|
12645
|
+
reporter.finalize();
|
|
12646
|
+
}
|
|
12647
|
+
});
|
|
12648
|
+
|
|
12649
|
+
const API_ASSIGNED_FIELDS = [
|
|
12650
|
+
"id",
|
|
12651
|
+
"created_at",
|
|
12652
|
+
"updated_at",
|
|
12653
|
+
"real_name",
|
|
12654
|
+
"all_presets",
|
|
12655
|
+
"image",
|
|
12656
|
+
"uuid"
|
|
12657
|
+
];
|
|
12658
|
+
function stripApiFields(payload) {
|
|
12659
|
+
const result = { ...payload };
|
|
12660
|
+
for (const field of API_ASSIGNED_FIELDS) {
|
|
12661
|
+
delete result[field];
|
|
12662
|
+
}
|
|
12663
|
+
return result;
|
|
12664
|
+
}
|
|
12665
|
+
async function listChangesets(basePath) {
|
|
12666
|
+
const dir = join(basePath, "schema", "changesets");
|
|
12667
|
+
if (!await fileExists(dir)) {
|
|
12668
|
+
return [];
|
|
12669
|
+
}
|
|
12670
|
+
const files = await readDirectory(dir);
|
|
12671
|
+
return files.filter((f) => f.endsWith(".json")).sort().reverse().map((f) => join(dir, f));
|
|
12672
|
+
}
|
|
12673
|
+
async function loadChangeset(filePath) {
|
|
12674
|
+
const content = await readFile$1(filePath, "utf-8");
|
|
12675
|
+
return JSON.parse(content);
|
|
12676
|
+
}
|
|
12677
|
+
function buildRollbackOps(changeset) {
|
|
12678
|
+
if (changeset.changes.length === 0) {
|
|
12679
|
+
return [];
|
|
12680
|
+
}
|
|
12681
|
+
return changeset.changes.map((entry) => {
|
|
12682
|
+
switch (entry.action) {
|
|
12683
|
+
case "create":
|
|
12684
|
+
return { type: entry.type, name: entry.name, action: "delete", payload: {} };
|
|
12685
|
+
case "update":
|
|
12686
|
+
return { type: entry.type, name: entry.name, action: "update", payload: entry.before ?? {} };
|
|
12687
|
+
case "delete":
|
|
12688
|
+
return { type: entry.type, name: entry.name, action: "create", payload: entry.before ?? {} };
|
|
12689
|
+
default:
|
|
12690
|
+
return { type: entry.type, name: entry.name, action: entry.action, payload: {} };
|
|
12691
|
+
}
|
|
12692
|
+
});
|
|
12693
|
+
}
|
|
12694
|
+
function rollbackAction(original) {
|
|
12695
|
+
switch (original) {
|
|
12696
|
+
case "create":
|
|
12697
|
+
return "delete";
|
|
12698
|
+
case "update":
|
|
12699
|
+
return "update";
|
|
12700
|
+
case "delete":
|
|
12701
|
+
return "create";
|
|
12702
|
+
}
|
|
12703
|
+
}
|
|
12704
|
+
function formatRollbackOutput(changes) {
|
|
12705
|
+
const byType = {
|
|
12706
|
+
folder: [],
|
|
12707
|
+
component: [],
|
|
12708
|
+
datasource: []
|
|
12709
|
+
};
|
|
12710
|
+
for (const entry of changes) {
|
|
12711
|
+
byType[entry.type]?.push(entry);
|
|
12712
|
+
}
|
|
12713
|
+
const icons = {
|
|
12714
|
+
create: chalk.green("+"),
|
|
12715
|
+
update: chalk.yellow("~"),
|
|
12716
|
+
delete: chalk.red("-")
|
|
12717
|
+
};
|
|
12718
|
+
const lines = [];
|
|
12719
|
+
const sections = [
|
|
12720
|
+
["Folders", byType.folder],
|
|
12721
|
+
["Components", byType.component],
|
|
12722
|
+
["Datasources", byType.datasource]
|
|
12723
|
+
];
|
|
12724
|
+
for (const [label, entries] of sections) {
|
|
12725
|
+
if (entries.length === 0) {
|
|
12726
|
+
continue;
|
|
12727
|
+
}
|
|
12728
|
+
lines.push(chalk.bold(label));
|
|
12729
|
+
for (const entry of entries) {
|
|
12730
|
+
const action = rollbackAction(entry.action);
|
|
12731
|
+
const icon = icons[action] ?? " ";
|
|
12732
|
+
const name = action === "delete" ? chalk.red(entry.name) : entry.name;
|
|
12733
|
+
lines.push(` ${icon} ${name} ${chalk.dim(`(${action})`)}`);
|
|
12734
|
+
if (entry.action === "update" && entry.before && entry.after) {
|
|
12735
|
+
let fromStr;
|
|
12736
|
+
let toStr;
|
|
12737
|
+
if (entry.type === "component") {
|
|
12738
|
+
fromStr = serializeComponent(applyDefaults(entry.after, COMPONENT_DEFAULTS));
|
|
12739
|
+
toStr = serializeComponent(applyDefaults(entry.before, COMPONENT_DEFAULTS));
|
|
12740
|
+
} else if (entry.type === "datasource") {
|
|
12741
|
+
fromStr = serializeDatasource(entry.after);
|
|
12742
|
+
toStr = serializeDatasource(entry.before);
|
|
12743
|
+
} else {
|
|
12744
|
+
continue;
|
|
12745
|
+
}
|
|
12746
|
+
if (fromStr !== toStr) {
|
|
12747
|
+
const patch = createTwoFilesPatch(
|
|
12748
|
+
`current/${entry.name}`,
|
|
12749
|
+
`restore/${entry.name}`,
|
|
12750
|
+
fromStr,
|
|
12751
|
+
toStr,
|
|
12752
|
+
"current",
|
|
12753
|
+
"restore"
|
|
12754
|
+
);
|
|
12755
|
+
for (const line of patch.split("\n")) {
|
|
12756
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
12757
|
+
lines.push(` ${chalk.green(line)}`);
|
|
12758
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
12759
|
+
lines.push(` ${chalk.red(line)}`);
|
|
12760
|
+
}
|
|
12761
|
+
}
|
|
12762
|
+
}
|
|
12763
|
+
}
|
|
12764
|
+
}
|
|
12765
|
+
lines.push("");
|
|
12766
|
+
}
|
|
12767
|
+
return lines.join("\n").trimEnd();
|
|
12768
|
+
}
|
|
12769
|
+
function buildGroupRefByPath(remote) {
|
|
12770
|
+
const folders = [...remote.componentFolders.values()];
|
|
12771
|
+
const pathByUuid = buildGroupPathByUuid(folders);
|
|
12772
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
12773
|
+
for (const folder of folders) {
|
|
12774
|
+
const segments = pathByUuid.get(folder.uuid);
|
|
12775
|
+
if (segments?.length) {
|
|
12776
|
+
byPath.set(segments.join("/"), { id: folder.id, uuid: folder.uuid });
|
|
12777
|
+
}
|
|
12778
|
+
}
|
|
12779
|
+
return byPath;
|
|
12780
|
+
}
|
|
12781
|
+
function parentPathOf(path) {
|
|
12782
|
+
const segments = path.split("/");
|
|
12783
|
+
return segments.length > 1 ? segments.slice(0, -1).join("/") : null;
|
|
12784
|
+
}
|
|
12785
|
+
function remapGroupUuids(payload, uuidMap) {
|
|
12786
|
+
if (uuidMap.size === 0) {
|
|
12787
|
+
return payload;
|
|
12788
|
+
}
|
|
12789
|
+
const result = { ...payload };
|
|
12790
|
+
if (typeof result.component_group_uuid === "string") {
|
|
12791
|
+
result.component_group_uuid = uuidMap.get(result.component_group_uuid) ?? result.component_group_uuid;
|
|
12792
|
+
}
|
|
12793
|
+
if (isRecord(result.schema)) {
|
|
12794
|
+
const schema = {};
|
|
12795
|
+
for (const [key, field] of Object.entries(result.schema)) {
|
|
12796
|
+
if (isRecord(field) && Array.isArray(field.component_group_whitelist)) {
|
|
12797
|
+
schema[key] = {
|
|
12798
|
+
...field,
|
|
12799
|
+
component_group_whitelist: field.component_group_whitelist.map((uuid) => typeof uuid === "string" ? uuidMap.get(uuid) ?? uuid : uuid)
|
|
12800
|
+
};
|
|
12801
|
+
} else {
|
|
12802
|
+
schema[key] = field;
|
|
12803
|
+
}
|
|
12804
|
+
}
|
|
12805
|
+
result.schema = schema;
|
|
12806
|
+
}
|
|
12807
|
+
return result;
|
|
12808
|
+
}
|
|
12809
|
+
async function executeRollback(spaceId, ops, remote) {
|
|
12810
|
+
const client = getMapiClient();
|
|
12811
|
+
const spaceIdNum = Number(spaceId);
|
|
12812
|
+
let created = 0;
|
|
12813
|
+
let updated = 0;
|
|
12814
|
+
let deleted = 0;
|
|
12815
|
+
const componentOps = ops.filter((op) => op.type === "component");
|
|
12816
|
+
const datasourceOps = ops.filter((op) => op.type === "datasource");
|
|
12817
|
+
const folderOps = ops.filter((op) => op.type === "folder");
|
|
12818
|
+
const groupRefByPath = buildGroupRefByPath(remote);
|
|
12819
|
+
const groupUuidMap = /* @__PURE__ */ new Map();
|
|
12820
|
+
const folderCreates = folderOps.filter((o) => o.action === "create").sort((a, b) => a.name.split("/").length - b.name.split("/").length);
|
|
12821
|
+
for (const op of folderCreates) {
|
|
12822
|
+
const existing = groupRefByPath.get(op.name);
|
|
12823
|
+
const oldUuid = typeof op.payload.uuid === "string" ? op.payload.uuid : void 0;
|
|
12824
|
+
if (existing) {
|
|
12825
|
+
if (oldUuid) {
|
|
12826
|
+
groupUuidMap.set(oldUuid, existing.uuid);
|
|
12827
|
+
}
|
|
12828
|
+
continue;
|
|
12829
|
+
}
|
|
12830
|
+
const parentPath = parentPathOf(op.name);
|
|
12831
|
+
const parentId = parentPath ? groupRefByPath.get(parentPath)?.id ?? null : null;
|
|
12832
|
+
const name = typeof op.payload.name === "string" ? op.payload.name : op.name.split("/").pop() ?? op.name;
|
|
12833
|
+
try {
|
|
12834
|
+
const res = await client.componentFolders.create({
|
|
12835
|
+
path: { space_id: spaceIdNum },
|
|
12836
|
+
body: { component_group: { name, parent_id: parentId } },
|
|
12837
|
+
throwOnError: true
|
|
12838
|
+
});
|
|
12839
|
+
const group = res.data?.component_group;
|
|
12840
|
+
if (group?.id != null && group.uuid) {
|
|
12841
|
+
groupRefByPath.set(op.name, { id: group.id, uuid: group.uuid });
|
|
12842
|
+
if (oldUuid) {
|
|
12843
|
+
groupUuidMap.set(oldUuid, group.uuid);
|
|
12844
|
+
}
|
|
12845
|
+
created++;
|
|
12846
|
+
}
|
|
12847
|
+
} catch (error) {
|
|
12848
|
+
handleAPIError("push_component_folder", error, `Failed to recreate folder ${op.name}`);
|
|
12849
|
+
}
|
|
12850
|
+
}
|
|
12851
|
+
for (const op of componentOps.filter((o) => o.action !== "delete")) {
|
|
12852
|
+
if (op.action === "create") {
|
|
12853
|
+
const payload = toComponentCreate(remapGroupUuids(stripApiFields(op.payload), groupUuidMap));
|
|
12854
|
+
try {
|
|
12855
|
+
await client.components.create({
|
|
12856
|
+
path: { space_id: spaceIdNum },
|
|
12857
|
+
body: { component: payload },
|
|
12858
|
+
throwOnError: true
|
|
12859
|
+
});
|
|
12860
|
+
created++;
|
|
12861
|
+
} catch (error) {
|
|
12862
|
+
handleAPIError("push_component", error, `Failed to create component ${op.name}`);
|
|
12863
|
+
}
|
|
12864
|
+
} else if (op.action === "update") {
|
|
12865
|
+
const existing = remote.components.get(op.name);
|
|
12866
|
+
if (existing?.id) {
|
|
12867
|
+
const payload = toComponentUpdate(remapGroupUuids(stripApiFields(op.payload), groupUuidMap));
|
|
12868
|
+
try {
|
|
12869
|
+
await client.components.update(existing.id, {
|
|
12870
|
+
path: { space_id: spaceIdNum },
|
|
12871
|
+
body: { component: payload },
|
|
12872
|
+
throwOnError: true
|
|
12873
|
+
});
|
|
12874
|
+
updated++;
|
|
12875
|
+
} catch (error) {
|
|
12876
|
+
handleAPIError("update_component", error, `Failed to update component ${op.name}`);
|
|
12877
|
+
}
|
|
12878
|
+
}
|
|
12879
|
+
}
|
|
12880
|
+
}
|
|
12881
|
+
for (const op of datasourceOps.filter((o) => o.action !== "delete")) {
|
|
12882
|
+
if (op.action === "create") {
|
|
12883
|
+
const payload = toDatasourceCreate(stripApiFields(op.payload));
|
|
12884
|
+
try {
|
|
12885
|
+
await client.datasources.create({
|
|
12886
|
+
path: { space_id: spaceIdNum },
|
|
12887
|
+
body: { datasource: payload },
|
|
12888
|
+
throwOnError: true
|
|
12889
|
+
});
|
|
12890
|
+
created++;
|
|
12891
|
+
} catch (error) {
|
|
12892
|
+
handleAPIError("push_datasource", error, `Failed to create datasource ${op.name}`);
|
|
12893
|
+
}
|
|
12894
|
+
} else if (op.action === "update") {
|
|
12895
|
+
const existing = remote.datasources.get(op.name);
|
|
12896
|
+
if (existing?.id) {
|
|
12897
|
+
const payload = toDatasourceUpdate(stripApiFields(op.payload), existing);
|
|
12898
|
+
try {
|
|
12899
|
+
await client.datasources.update(existing.id, {
|
|
12900
|
+
path: { space_id: spaceIdNum },
|
|
12901
|
+
body: { datasource: payload },
|
|
12902
|
+
throwOnError: true
|
|
12903
|
+
});
|
|
12904
|
+
updated++;
|
|
12905
|
+
} catch (error) {
|
|
12906
|
+
handleAPIError("update_datasource", error, `Failed to update datasource ${op.name}`);
|
|
12907
|
+
}
|
|
12908
|
+
}
|
|
12909
|
+
}
|
|
12910
|
+
}
|
|
12911
|
+
for (const op of datasourceOps.filter((o) => o.action === "delete")) {
|
|
12912
|
+
const existing = remote.datasources.get(op.name);
|
|
12913
|
+
if (existing?.id) {
|
|
12914
|
+
try {
|
|
12915
|
+
await client.datasources.delete(existing.id, {
|
|
12916
|
+
path: { space_id: spaceIdNum },
|
|
12917
|
+
throwOnError: true
|
|
12918
|
+
});
|
|
12919
|
+
deleted++;
|
|
12920
|
+
} catch (error) {
|
|
12921
|
+
handleAPIError("delete_datasource", error, `Failed to delete datasource ${op.name}`);
|
|
12922
|
+
}
|
|
12923
|
+
}
|
|
12924
|
+
}
|
|
12925
|
+
for (const op of componentOps.filter((o) => o.action === "delete")) {
|
|
12926
|
+
const existing = remote.components.get(op.name);
|
|
12927
|
+
if (existing?.id) {
|
|
12928
|
+
try {
|
|
12929
|
+
await client.components.delete(existing.id, {
|
|
12930
|
+
path: { space_id: spaceIdNum },
|
|
12931
|
+
throwOnError: true
|
|
12932
|
+
});
|
|
12933
|
+
deleted++;
|
|
12934
|
+
} catch (error) {
|
|
12935
|
+
handleAPIError("delete_component", error, `Failed to delete component ${op.name}`);
|
|
12936
|
+
}
|
|
12937
|
+
}
|
|
12938
|
+
}
|
|
12939
|
+
const folderDeletes = folderOps.filter((o) => o.action === "delete").sort((a, b) => b.name.split("/").length - a.name.split("/").length);
|
|
12940
|
+
for (const op of folderDeletes) {
|
|
12941
|
+
const ref = groupRefByPath.get(op.name);
|
|
12942
|
+
if (!ref) {
|
|
12943
|
+
continue;
|
|
12944
|
+
}
|
|
12945
|
+
try {
|
|
12946
|
+
await client.componentFolders.delete(ref.id, {
|
|
12947
|
+
path: { space_id: spaceIdNum },
|
|
12948
|
+
throwOnError: true
|
|
12949
|
+
});
|
|
12950
|
+
deleted++;
|
|
12951
|
+
} catch (error) {
|
|
12952
|
+
handleAPIError("delete_component_folder", error, `Failed to delete folder ${op.name}`);
|
|
12953
|
+
}
|
|
12954
|
+
}
|
|
12955
|
+
return { created, updated, deleted };
|
|
12956
|
+
}
|
|
12957
|
+
|
|
12958
|
+
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) => {
|
|
12959
|
+
const ui = getUI();
|
|
12960
|
+
const logger = getLogger();
|
|
12961
|
+
const reporter = getReporter();
|
|
12962
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
12963
|
+
const { state } = session();
|
|
12964
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Rolling back schema...");
|
|
12965
|
+
logger.info("Schema rollback started", { changesetFile, space });
|
|
12966
|
+
if (!requireAuthentication(state, verbose)) {
|
|
12967
|
+
return;
|
|
12968
|
+
}
|
|
12969
|
+
if (!space) {
|
|
12970
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
12971
|
+
return;
|
|
12972
|
+
}
|
|
12973
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
12974
|
+
try {
|
|
12975
|
+
const resolvedBase = resolvePath(basePath, "");
|
|
12976
|
+
let resolvedFile;
|
|
12977
|
+
if (changesetFile) {
|
|
12978
|
+
resolvedFile = changesetFile;
|
|
12979
|
+
} else if (options.latest) {
|
|
12980
|
+
const available = await listChangesets(resolvedBase);
|
|
12981
|
+
if (available.length === 0) {
|
|
12982
|
+
ui.warn("No changesets found. Run `schema push` first to create one.");
|
|
12983
|
+
return;
|
|
12984
|
+
}
|
|
12985
|
+
resolvedFile = available[0];
|
|
12986
|
+
ui.info(`Using latest changeset: ${basename(resolvedFile)}`);
|
|
12987
|
+
} else {
|
|
12988
|
+
const available = await listChangesets(resolvedBase);
|
|
12989
|
+
if (available.length === 0) {
|
|
12990
|
+
ui.warn("No changesets found. Run `schema push` first to create one.");
|
|
12991
|
+
return;
|
|
12992
|
+
}
|
|
12993
|
+
resolvedFile = await select({
|
|
12994
|
+
message: "Select a changeset to roll back:",
|
|
12995
|
+
choices: available.map((f) => ({ name: basename(f), value: f }))
|
|
12996
|
+
});
|
|
12997
|
+
}
|
|
12998
|
+
let changeset;
|
|
12999
|
+
try {
|
|
13000
|
+
changeset = await loadChangeset(resolvedFile);
|
|
13001
|
+
} catch (maybeError) {
|
|
13002
|
+
handleError(toError(maybeError), verbose);
|
|
13003
|
+
return;
|
|
13004
|
+
}
|
|
13005
|
+
logger.info("Changeset loaded", { file: resolvedFile, changes: changeset.changes.length });
|
|
13006
|
+
const ops = buildRollbackOps(changeset);
|
|
13007
|
+
if (ops.length === 0) {
|
|
13008
|
+
ui.ok("Changeset has no changes \u2014 nothing to roll back.");
|
|
13009
|
+
return;
|
|
13010
|
+
}
|
|
13011
|
+
ui.br();
|
|
13012
|
+
ui.log(formatRollbackOutput(changeset.changes));
|
|
13013
|
+
if (options.dryRun) {
|
|
13014
|
+
ui.info("Dry run \u2014 no changes applied.");
|
|
13015
|
+
logger.info("Dry run completed", { ops: ops.length });
|
|
13016
|
+
return;
|
|
13017
|
+
}
|
|
13018
|
+
if (!options.yes) {
|
|
13019
|
+
const confirmed = await confirm({
|
|
13020
|
+
message: `Apply rollback of ${ops.length} change(s) from ${basename(resolvedFile)}?`,
|
|
13021
|
+
default: false
|
|
13022
|
+
});
|
|
13023
|
+
if (!confirmed) {
|
|
13024
|
+
ui.info("Rollback cancelled.");
|
|
13025
|
+
return;
|
|
13026
|
+
}
|
|
13027
|
+
}
|
|
13028
|
+
const remoteSpinner = ui.createSpinner(`Fetching current remote state from space ${space}...`);
|
|
13029
|
+
let remoteResult;
|
|
13030
|
+
try {
|
|
13031
|
+
remoteResult = await fetchRemoteSchema(space);
|
|
13032
|
+
} catch (maybeError) {
|
|
13033
|
+
remoteSpinner.failed("Failed to fetch remote schema");
|
|
13034
|
+
handleError(toError(maybeError), verbose);
|
|
13035
|
+
return;
|
|
13036
|
+
}
|
|
13037
|
+
const { remote } = remoteResult;
|
|
13038
|
+
remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
|
|
13039
|
+
const rollbackSpinner = ui.createSpinner("Applying rollback...");
|
|
13040
|
+
let result;
|
|
13041
|
+
try {
|
|
13042
|
+
result = await executeRollback(space, ops, remote);
|
|
13043
|
+
} catch (error) {
|
|
13044
|
+
rollbackSpinner.failed("Failed to apply rollback");
|
|
13045
|
+
throw error;
|
|
13046
|
+
}
|
|
13047
|
+
summary.total = result.created + result.updated + result.deleted;
|
|
13048
|
+
summary.succeeded = summary.total;
|
|
13049
|
+
rollbackSpinner.succeed(`Rolled back: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted.`);
|
|
13050
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
13051
|
+
const rollbackChangesetPath = await saveChangeset(resolvedBase, {
|
|
13052
|
+
timestamp,
|
|
13053
|
+
spaceId: Number(space),
|
|
13054
|
+
remote: { components: remoteResult.rawComponents, componentFolders: remoteResult.rawComponentFolders, datasources: remoteResult.rawDatasources },
|
|
13055
|
+
changes: ops.map((op) => ({
|
|
13056
|
+
type: op.type,
|
|
13057
|
+
name: op.name,
|
|
13058
|
+
action: op.action,
|
|
13059
|
+
...Object.keys(op.payload).length > 0 && { after: op.payload }
|
|
13060
|
+
}))
|
|
13061
|
+
});
|
|
13062
|
+
logger.info("Rollback changeset saved", { path: displayPath(rollbackChangesetPath, basePath) });
|
|
13063
|
+
} catch (maybeError) {
|
|
13064
|
+
summary.failed += 1;
|
|
13065
|
+
handleError(toError(maybeError), verbose);
|
|
13066
|
+
} finally {
|
|
13067
|
+
logger.info("Schema rollback finished", { summary });
|
|
13068
|
+
reporter.addSummary("schemaRollbackResults", summary);
|
|
13069
|
+
reporter.finalize();
|
|
13070
|
+
}
|
|
13071
|
+
});
|
|
13072
|
+
|
|
10632
13073
|
const program = getProgram();
|
|
10633
13074
|
konsola.br();
|
|
10634
13075
|
konsola.br();
|