storyblok 4.18.2 → 4.19.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 +2057 -157
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
package/dist/index.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { homedir } from 'node:os';
|
|
|
7
7
|
import { loadConfig as loadConfig$1, SUPPORTED_EXTENSIONS } from 'c12';
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import { readPackageUp } from 'read-package-up';
|
|
10
|
-
import { Command } from 'commander';
|
|
10
|
+
import { Command, Option } from 'commander';
|
|
11
11
|
import { MultiBar, Presets } from 'cli-progress';
|
|
12
12
|
import { Spinner } from '@topcli/spinner';
|
|
13
13
|
import fs, { mkdir, writeFile, readdir, readFile as readFile$1, appendFile, access, constants, unlink } from 'node:fs/promises';
|
|
@@ -26,6 +26,7 @@ import { Octokit } from 'octokit';
|
|
|
26
26
|
import { pipeline as pipeline$1 } from 'node:stream/promises';
|
|
27
27
|
import { Buffer } from 'node:buffer';
|
|
28
28
|
import Storyblok from 'storyblok-js-client';
|
|
29
|
+
import { createTwoFilesPatch } from 'diff';
|
|
29
30
|
|
|
30
31
|
const commands = {
|
|
31
32
|
LOGIN: "login",
|
|
@@ -41,7 +42,8 @@ const commands = {
|
|
|
41
42
|
LOGS: "logs",
|
|
42
43
|
REPORTS: "reports",
|
|
43
44
|
ASSETS: "assets",
|
|
44
|
-
STORIES: "stories"
|
|
45
|
+
STORIES: "stories",
|
|
46
|
+
SCHEMA: "schema"
|
|
45
47
|
};
|
|
46
48
|
const colorPalette = {
|
|
47
49
|
PRIMARY: "#8d60ff",
|
|
@@ -61,7 +63,8 @@ const colorPalette = {
|
|
|
61
63
|
LOGS: "#4ade80",
|
|
62
64
|
REPORTS: "#4ade80",
|
|
63
65
|
ASSETS: "#f97316",
|
|
64
|
-
STORIES: "#a185ff"
|
|
66
|
+
STORIES: "#a185ff",
|
|
67
|
+
SCHEMA: "#e91e63"
|
|
65
68
|
};
|
|
66
69
|
const regions = {
|
|
67
70
|
EU: "eu",
|
|
@@ -702,6 +705,7 @@ const API_ACTIONS = {
|
|
|
702
705
|
update_component_group: "Failed to update component group",
|
|
703
706
|
update_component_preset: "Failed to update component preset",
|
|
704
707
|
delete_component_preset: "Failed to delete component preset",
|
|
708
|
+
delete_component: "Failed to delete component",
|
|
705
709
|
pull_stories: "Failed to pull stories",
|
|
706
710
|
pull_story: "Failed to pull story",
|
|
707
711
|
create_story: "Failed to create story",
|
|
@@ -720,6 +724,8 @@ const API_ACTIONS = {
|
|
|
720
724
|
push_datasource: "Failed to push datasource",
|
|
721
725
|
update_datasource: "Failed to update datasource",
|
|
722
726
|
delete_datasource: "Failed to delete datasource",
|
|
727
|
+
push_datasource_entry: "Failed to push datasource entry",
|
|
728
|
+
update_datasource_entry: "Failed to update datasource entry",
|
|
723
729
|
delete_datasource_entry: "Failed to delete datasource entry",
|
|
724
730
|
create_space: "Failed to create space",
|
|
725
731
|
pull_spaces: "Failed to pull spaces",
|
|
@@ -1048,11 +1054,11 @@ function requireAuthentication(state, verbose = false) {
|
|
|
1048
1054
|
return true;
|
|
1049
1055
|
}
|
|
1050
1056
|
|
|
1051
|
-
const toCamelCase = (str) => {
|
|
1057
|
+
const toCamelCase$1 = (str) => {
|
|
1052
1058
|
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/_/g, "").replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/[^a-z0-9]([a-z])/gi, (_, letter) => letter.toUpperCase()).replace(/[^a-z0-9]/gi, "");
|
|
1053
1059
|
};
|
|
1054
1060
|
const toPascalCase = (str) => {
|
|
1055
|
-
const camelCase = toCamelCase(str);
|
|
1061
|
+
const camelCase = toCamelCase$1(str);
|
|
1056
1062
|
return camelCase ? camelCase[0].toUpperCase() + camelCase.slice(1) : camelCase;
|
|
1057
1063
|
};
|
|
1058
1064
|
const capitalize = (str) => {
|
|
@@ -1069,6 +1075,7 @@ function maskToken(token) {
|
|
|
1069
1075
|
const maskedPart = "*".repeat(token.length - 4);
|
|
1070
1076
|
return `${visiblePart}${maskedPart}`;
|
|
1071
1077
|
}
|
|
1078
|
+
const slugify = (text) => text.toString().toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]+/g, "").replace(/-{2,}/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
1072
1079
|
function createRegexFromGlob(pattern) {
|
|
1073
1080
|
return new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*")}$`);
|
|
1074
1081
|
}
|
|
@@ -1150,6 +1157,25 @@ function getPackageJson() {
|
|
|
1150
1157
|
return packageJson$1;
|
|
1151
1158
|
}
|
|
1152
1159
|
|
|
1160
|
+
async function fetchAllPages(fetchFunction, extractDataFunction) {
|
|
1161
|
+
const items = [];
|
|
1162
|
+
let page = 1;
|
|
1163
|
+
while (true) {
|
|
1164
|
+
const { data, response } = await fetchFunction(page);
|
|
1165
|
+
const totalHeader = response.headers.get("total");
|
|
1166
|
+
const fetchedItems = extractDataFunction(data);
|
|
1167
|
+
items.push(...fetchedItems);
|
|
1168
|
+
if (!totalHeader) {
|
|
1169
|
+
return items;
|
|
1170
|
+
}
|
|
1171
|
+
const total = Number(totalHeader);
|
|
1172
|
+
if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
|
|
1173
|
+
return items;
|
|
1174
|
+
}
|
|
1175
|
+
page++;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1153
1179
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
1154
1180
|
const __dirname$1 = dirname(__filename$1);
|
|
1155
1181
|
function isRegion(value) {
|
|
@@ -1238,6 +1264,10 @@ class UI {
|
|
|
1238
1264
|
this.br();
|
|
1239
1265
|
}
|
|
1240
1266
|
}
|
|
1267
|
+
/** Plain console.log passthrough — use for preformatted or multi-line text. */
|
|
1268
|
+
log(message) {
|
|
1269
|
+
this.console?.log(message);
|
|
1270
|
+
}
|
|
1241
1271
|
list(items) {
|
|
1242
1272
|
for (const item of items) {
|
|
1243
1273
|
this.console?.log(` ${item}`);
|
|
@@ -2147,14 +2177,14 @@ async function performInteractiveLogin(options) {
|
|
|
2147
2177
|
}
|
|
2148
2178
|
}
|
|
2149
2179
|
|
|
2150
|
-
const program$
|
|
2180
|
+
const program$f = getProgram();
|
|
2151
2181
|
const allRegionsText = Object.values(regions).join(",");
|
|
2152
|
-
program$
|
|
2182
|
+
program$f.command(commands.LOGIN).description("Login to the Storyblok CLI").option("-t, --token <token>", "Token to login directly without questions, like for CI environments").option(
|
|
2153
2183
|
"-r, --region <region>",
|
|
2154
2184
|
`The region you would like to work in. Please keep in mind that the region must match the region of your space. This region flag will be used for the other cli's commands. You can use the values: ${allRegionsText}.`
|
|
2155
2185
|
).action(async (options) => {
|
|
2156
2186
|
konsola.title(`${commands.LOGIN}`, colorPalette.LOGIN);
|
|
2157
|
-
const verbose = program$
|
|
2187
|
+
const verbose = program$f.opts().verbose;
|
|
2158
2188
|
const { token, region } = options;
|
|
2159
2189
|
const { state, updateSession, persistCredentials } = session();
|
|
2160
2190
|
if (state.isLoggedIn && !state.envLogin) {
|
|
@@ -2212,10 +2242,10 @@ program$e.command(commands.LOGIN).description("Login to the Storyblok CLI").opti
|
|
|
2212
2242
|
konsola.br();
|
|
2213
2243
|
});
|
|
2214
2244
|
|
|
2215
|
-
const program$
|
|
2216
|
-
program$
|
|
2245
|
+
const program$e = getProgram();
|
|
2246
|
+
program$e.command(commands.LOGOUT).description("Logout from the Storyblok CLI").action(async () => {
|
|
2217
2247
|
konsola.title(`${commands.LOGOUT}`, colorPalette.LOGOUT);
|
|
2218
|
-
const verbose = program$
|
|
2248
|
+
const verbose = program$e.opts().verbose;
|
|
2219
2249
|
try {
|
|
2220
2250
|
const { state } = session();
|
|
2221
2251
|
if (!state.isLoggedIn || !state.password || !state.region) {
|
|
@@ -2262,10 +2292,10 @@ async function openSignupInBrowser(url) {
|
|
|
2262
2292
|
}
|
|
2263
2293
|
}
|
|
2264
2294
|
|
|
2265
|
-
const program$
|
|
2266
|
-
program$
|
|
2295
|
+
const program$d = getProgram();
|
|
2296
|
+
program$d.command(commands.SIGNUP).description("Sign up for Storyblok").action(async () => {
|
|
2267
2297
|
konsola.title(`${commands.SIGNUP}`, colorPalette.SIGNUP);
|
|
2268
|
-
const verbose = program$
|
|
2298
|
+
const verbose = program$d.opts().verbose;
|
|
2269
2299
|
const { state } = session();
|
|
2270
2300
|
if (state.isLoggedIn && !state.envLogin) {
|
|
2271
2301
|
konsola.ok(`You are already logged in. If you want to signup with a different account, please logout first.`);
|
|
@@ -2286,10 +2316,10 @@ program$c.command(commands.SIGNUP).description("Sign up for Storyblok").action(a
|
|
|
2286
2316
|
konsola.br();
|
|
2287
2317
|
});
|
|
2288
2318
|
|
|
2289
|
-
const program$
|
|
2290
|
-
program$
|
|
2319
|
+
const program$c = getProgram();
|
|
2320
|
+
program$c.command(commands.USER).description("Get the current user").action(async () => {
|
|
2291
2321
|
konsola.title(`${commands.USER}`, colorPalette.USER);
|
|
2292
|
-
const verbose = program$
|
|
2322
|
+
const verbose = program$c.opts().verbose;
|
|
2293
2323
|
const { state } = session();
|
|
2294
2324
|
if (!requireAuthentication(state)) {
|
|
2295
2325
|
return;
|
|
@@ -2317,10 +2347,10 @@ program$b.command(commands.USER).description("Get the current user").action(asyn
|
|
|
2317
2347
|
konsola.br();
|
|
2318
2348
|
});
|
|
2319
2349
|
|
|
2320
|
-
const program$
|
|
2321
|
-
const componentsCommand = program$
|
|
2350
|
+
const program$b = getProgram();
|
|
2351
|
+
const componentsCommand = program$b.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`);
|
|
2322
2352
|
|
|
2323
|
-
function isComponent(item) {
|
|
2353
|
+
function isComponent$1(item) {
|
|
2324
2354
|
return "schema" in item;
|
|
2325
2355
|
}
|
|
2326
2356
|
function isPreset(item) {
|
|
@@ -2354,7 +2384,7 @@ async function loadComponents(directoryPath, options) {
|
|
|
2354
2384
|
throw new Error('Internal tag is missing "id"!');
|
|
2355
2385
|
}
|
|
2356
2386
|
tagMap.set(item.id, item);
|
|
2357
|
-
} else if (isComponent(item)) {
|
|
2387
|
+
} else if (isComponent$1(item)) {
|
|
2358
2388
|
const existing = componentMap.get(item.name);
|
|
2359
2389
|
if (existing) {
|
|
2360
2390
|
duplicates.push(`Component "${item.name}" found in both "${existing.file}" and "${file}"`);
|
|
@@ -2390,44 +2420,20 @@ To fix this, either:
|
|
|
2390
2420
|
}
|
|
2391
2421
|
|
|
2392
2422
|
const DEFAULT_COMPONENTS_FILENAME = "components";
|
|
2393
|
-
const DEFAULT_GROUPS_FILENAME = "groups";
|
|
2423
|
+
const DEFAULT_GROUPS_FILENAME$1 = "groups";
|
|
2394
2424
|
const DEFAULT_PRESETS_FILENAME = "presets";
|
|
2395
2425
|
const DEFAULT_TAGS_FILENAME = "tags";
|
|
2396
2426
|
|
|
2397
|
-
async function fetchAllPages(fetchFunction, extractDataFunction) {
|
|
2398
|
-
const items = [];
|
|
2399
|
-
let page = 1;
|
|
2400
|
-
while (true) {
|
|
2401
|
-
const { data, response } = await fetchFunction(page);
|
|
2402
|
-
const totalHeader = response.headers.get("total");
|
|
2403
|
-
const fetchedItems = extractDataFunction(data);
|
|
2404
|
-
items.push(...fetchedItems);
|
|
2405
|
-
if (!totalHeader) {
|
|
2406
|
-
return items;
|
|
2407
|
-
}
|
|
2408
|
-
const total = Number(totalHeader);
|
|
2409
|
-
if (Number.isNaN(total) || items.length >= total || fetchedItems.length === 0) {
|
|
2410
|
-
return items;
|
|
2411
|
-
}
|
|
2412
|
-
page++;
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
2427
|
const fetchComponents = async (spaceId) => {
|
|
2417
2428
|
try {
|
|
2418
2429
|
const client = getMapiClient();
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
},
|
|
2427
|
-
throwOnError: true
|
|
2428
|
-
}),
|
|
2429
|
-
(data) => data?.components ?? []
|
|
2430
|
-
);
|
|
2430
|
+
const { data } = await client.components.list({
|
|
2431
|
+
path: {
|
|
2432
|
+
space_id: Number(spaceId)
|
|
2433
|
+
},
|
|
2434
|
+
throwOnError: true
|
|
2435
|
+
});
|
|
2436
|
+
return data?.components ?? [];
|
|
2431
2437
|
} catch (error) {
|
|
2432
2438
|
handleAPIError("pull_components", error);
|
|
2433
2439
|
}
|
|
@@ -2435,19 +2441,16 @@ const fetchComponents = async (spaceId) => {
|
|
|
2435
2441
|
const fetchComponent = async (spaceId, componentName) => {
|
|
2436
2442
|
try {
|
|
2437
2443
|
const client = getMapiClient();
|
|
2438
|
-
const
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
}),
|
|
2449
|
-
(data) => data?.components ?? []
|
|
2450
|
-
);
|
|
2444
|
+
const { data } = await client.components.list({
|
|
2445
|
+
path: {
|
|
2446
|
+
space_id: Number(spaceId)
|
|
2447
|
+
},
|
|
2448
|
+
query: {
|
|
2449
|
+
search: componentName
|
|
2450
|
+
},
|
|
2451
|
+
throwOnError: true
|
|
2452
|
+
});
|
|
2453
|
+
const matches = data?.components ?? [];
|
|
2451
2454
|
return matches.find((c) => c.name === componentName);
|
|
2452
2455
|
} catch (error) {
|
|
2453
2456
|
handleAPIError("pull_components", error, `Failed to fetch component ${componentName}`);
|
|
@@ -2514,7 +2517,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2514
2517
|
const presetsFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.${suffix}.json` : `${sanitizedName}.${DEFAULT_PRESETS_FILENAME}.json`);
|
|
2515
2518
|
await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2));
|
|
2516
2519
|
}
|
|
2517
|
-
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
|
|
2520
|
+
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
|
|
2518
2521
|
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
|
|
2519
2522
|
const internalTagsFilePath = join(resolvedPath, suffix ? `${DEFAULT_TAGS_FILENAME}.${suffix}.json` : `${DEFAULT_TAGS_FILENAME}.json`);
|
|
2520
2523
|
await saveToFile(internalTagsFilePath, JSON.stringify(internalTags, null, 2));
|
|
@@ -2524,7 +2527,7 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2524
2527
|
const componentsFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
|
|
2525
2528
|
await saveToFile(componentsFilePath, JSON.stringify(components, null, 2));
|
|
2526
2529
|
if (groups.length > 0) {
|
|
2527
|
-
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME}.json`);
|
|
2530
|
+
const groupsFilePath = join(resolvedPath, suffix ? `${DEFAULT_GROUPS_FILENAME$1}.${suffix}.json` : `${DEFAULT_GROUPS_FILENAME$1}.json`);
|
|
2528
2531
|
await saveToFile(groupsFilePath, JSON.stringify(groups, null, 2));
|
|
2529
2532
|
}
|
|
2530
2533
|
if (presets.length > 0) {
|
|
@@ -2540,6 +2543,27 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
|
|
|
2540
2543
|
}
|
|
2541
2544
|
};
|
|
2542
2545
|
|
|
2546
|
+
function isSchemaField$1(value) {
|
|
2547
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
2548
|
+
}
|
|
2549
|
+
function toWritableSchema(schema) {
|
|
2550
|
+
if (!schema) {
|
|
2551
|
+
return void 0;
|
|
2552
|
+
}
|
|
2553
|
+
const result = {};
|
|
2554
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2555
|
+
if (isSchemaField$1(value)) {
|
|
2556
|
+
result[key] = value;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
return result;
|
|
2560
|
+
}
|
|
2561
|
+
function toRequestTagIds(tagIds) {
|
|
2562
|
+
if (!tagIds) {
|
|
2563
|
+
return void 0;
|
|
2564
|
+
}
|
|
2565
|
+
return tagIds.map((id) => Number(id));
|
|
2566
|
+
}
|
|
2543
2567
|
const pushComponent = async (space, component) => {
|
|
2544
2568
|
try {
|
|
2545
2569
|
const client = getMapiClient();
|
|
@@ -2574,10 +2598,23 @@ const updateComponent = async (space, componentId, component) => {
|
|
|
2574
2598
|
}
|
|
2575
2599
|
};
|
|
2576
2600
|
const upsertComponent = async (space, component, existingId) => {
|
|
2601
|
+
const { name, display_name, schema, is_root, is_nestable, component_group_uuid, color, icon, preview_field, internal_tag_ids } = component;
|
|
2602
|
+
const payload = {
|
|
2603
|
+
name,
|
|
2604
|
+
display_name: display_name ?? void 0,
|
|
2605
|
+
schema: toWritableSchema(schema),
|
|
2606
|
+
is_root,
|
|
2607
|
+
is_nestable,
|
|
2608
|
+
component_group_uuid: component_group_uuid ?? void 0,
|
|
2609
|
+
color: color ?? void 0,
|
|
2610
|
+
icon: icon ?? void 0,
|
|
2611
|
+
preview_field: preview_field ?? void 0,
|
|
2612
|
+
internal_tag_ids: toRequestTagIds(internal_tag_ids)
|
|
2613
|
+
};
|
|
2577
2614
|
if (existingId) {
|
|
2578
|
-
return await updateComponent(space, existingId,
|
|
2615
|
+
return await updateComponent(space, existingId, payload);
|
|
2579
2616
|
} else {
|
|
2580
|
-
return await pushComponent(space,
|
|
2617
|
+
return await pushComponent(space, payload);
|
|
2581
2618
|
}
|
|
2582
2619
|
};
|
|
2583
2620
|
const pushComponentGroup = async (space, componentGroup) => {
|
|
@@ -2629,7 +2666,14 @@ const pushComponentPreset = async (space, preset) => {
|
|
|
2629
2666
|
space_id: Number(space)
|
|
2630
2667
|
},
|
|
2631
2668
|
body: {
|
|
2632
|
-
preset
|
|
2669
|
+
preset: {
|
|
2670
|
+
...preset,
|
|
2671
|
+
preset: preset.preset ?? void 0,
|
|
2672
|
+
image: preset.image ?? void 0,
|
|
2673
|
+
color: preset.color ?? void 0,
|
|
2674
|
+
icon: preset.icon ?? void 0,
|
|
2675
|
+
description: preset.description ?? void 0
|
|
2676
|
+
}
|
|
2633
2677
|
},
|
|
2634
2678
|
throwOnError: true
|
|
2635
2679
|
});
|
|
@@ -2646,7 +2690,14 @@ const updateComponentPreset = async (space, presetId, preset) => {
|
|
|
2646
2690
|
space_id: Number(space)
|
|
2647
2691
|
},
|
|
2648
2692
|
body: {
|
|
2649
|
-
preset
|
|
2693
|
+
preset: {
|
|
2694
|
+
...preset,
|
|
2695
|
+
preset: preset.preset ?? void 0,
|
|
2696
|
+
image: preset.image ?? void 0,
|
|
2697
|
+
color: preset.color ?? void 0,
|
|
2698
|
+
icon: preset.icon ?? void 0,
|
|
2699
|
+
description: preset.description ?? void 0
|
|
2700
|
+
}
|
|
2650
2701
|
},
|
|
2651
2702
|
throwOnError: true
|
|
2652
2703
|
});
|
|
@@ -2680,7 +2731,7 @@ const pushComponentInternalTag = async (space, componentInternalTag) => {
|
|
|
2680
2731
|
path: {
|
|
2681
2732
|
space_id: Number(space)
|
|
2682
2733
|
},
|
|
2683
|
-
body: componentInternalTag,
|
|
2734
|
+
body: { internal_tag: componentInternalTag },
|
|
2684
2735
|
throwOnError: true
|
|
2685
2736
|
});
|
|
2686
2737
|
return data.internal_tag;
|
|
@@ -2695,7 +2746,7 @@ const updateComponentInternalTag = async (space, tagId, componentInternalTag) =>
|
|
|
2695
2746
|
path: {
|
|
2696
2747
|
space_id: Number(space)
|
|
2697
2748
|
},
|
|
2698
|
-
body: componentInternalTag,
|
|
2749
|
+
body: { internal_tag: componentInternalTag },
|
|
2699
2750
|
throwOnError: true
|
|
2700
2751
|
});
|
|
2701
2752
|
return data.internal_tag;
|
|
@@ -3951,16 +4002,14 @@ const fetchSpace = async (spaceId) => {
|
|
|
3951
4002
|
handleAPIError("pull_spaces", error, `Failed to fetch space ${spaceId}`);
|
|
3952
4003
|
}
|
|
3953
4004
|
};
|
|
3954
|
-
const createSpace = async (space) => {
|
|
4005
|
+
const createSpace = async (space, query) => {
|
|
3955
4006
|
try {
|
|
3956
|
-
const { in_org, assign_partner, ...spaceData } = space;
|
|
3957
4007
|
const client = getMapiClient();
|
|
3958
4008
|
const { data } = await client.spaces.create({
|
|
3959
4009
|
body: {
|
|
3960
|
-
space
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
}
|
|
4010
|
+
space
|
|
4011
|
+
},
|
|
4012
|
+
query
|
|
3964
4013
|
});
|
|
3965
4014
|
return data?.space;
|
|
3966
4015
|
} catch (error) {
|
|
@@ -3971,10 +4020,10 @@ const createSpace = async (space) => {
|
|
|
3971
4020
|
const fetchLanguages = async (spaceId) => {
|
|
3972
4021
|
try {
|
|
3973
4022
|
const space = await fetchSpace(spaceId);
|
|
3974
|
-
if (space?.default_lang_name
|
|
4023
|
+
if (space?.default_lang_name && space?.languages?.length) {
|
|
3975
4024
|
return {
|
|
3976
|
-
default_lang_name: space
|
|
3977
|
-
languages: space
|
|
4025
|
+
default_lang_name: space.default_lang_name,
|
|
4026
|
+
languages: space.languages
|
|
3978
4027
|
};
|
|
3979
4028
|
}
|
|
3980
4029
|
} catch (error) {
|
|
@@ -3994,8 +4043,8 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
|
|
|
3994
4043
|
}
|
|
3995
4044
|
};
|
|
3996
4045
|
|
|
3997
|
-
const program$
|
|
3998
|
-
const languagesCommand = program$
|
|
4046
|
+
const program$a = getProgram();
|
|
4047
|
+
const languagesCommand = program$a.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`);
|
|
3999
4048
|
const pullCmd$3 = languagesCommand.command("pull").description(`Download your space's languages schema as json`).option("-f, --filename <filename>", "filename to save the file as <filename>.<suffix>.json").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. languages.<suffix>.json). By default, the space ID is used.").option("-s, --space <space>", "space ID");
|
|
4000
4049
|
pullCmd$3.action(async (options, command) => {
|
|
4001
4050
|
konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
|
|
@@ -4041,8 +4090,8 @@ pullCmd$3.action(async (options, command) => {
|
|
|
4041
4090
|
konsola.br();
|
|
4042
4091
|
});
|
|
4043
4092
|
|
|
4044
|
-
const program$
|
|
4045
|
-
const migrationsCommand = program$
|
|
4093
|
+
const program$9 = getProgram();
|
|
4094
|
+
const migrationsCommand = program$9.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`);
|
|
4046
4095
|
|
|
4047
4096
|
const getMigrationTemplate = () => {
|
|
4048
4097
|
return `export default function (block) {
|
|
@@ -4146,7 +4195,7 @@ const fetchStories = async (spaceId, params) => {
|
|
|
4146
4195
|
const fetchStory = async (spaceId, storyId) => {
|
|
4147
4196
|
try {
|
|
4148
4197
|
const client = getMapiClient();
|
|
4149
|
-
const { data } = await client.stories.get(storyId, {
|
|
4198
|
+
const { data } = await client.stories.get(Number(storyId), {
|
|
4150
4199
|
path: {
|
|
4151
4200
|
space_id: Number(spaceId)
|
|
4152
4201
|
},
|
|
@@ -4191,9 +4240,11 @@ const updateStory = async (spaceId, storyId, payload) => {
|
|
|
4191
4240
|
...payload.story,
|
|
4192
4241
|
// StoryUpdate2 expects `parent_id?: number`; normalize null → undefined.
|
|
4193
4242
|
parent_id: payload.story.parent_id ?? void 0
|
|
4194
|
-
}
|
|
4195
|
-
|
|
4196
|
-
|
|
4243
|
+
}
|
|
4244
|
+
},
|
|
4245
|
+
query: {
|
|
4246
|
+
force_update: payload.force_update === "1",
|
|
4247
|
+
...payload.publish ? { publish: Boolean(payload.publish) } : {}
|
|
4197
4248
|
},
|
|
4198
4249
|
throwOnError: true
|
|
4199
4250
|
});
|
|
@@ -4290,6 +4341,35 @@ const prefetchTargetStoriesByKeys = async (spaceId, keys, options) => {
|
|
|
4290
4341
|
return result;
|
|
4291
4342
|
};
|
|
4292
4343
|
|
|
4344
|
+
function parseFilterQuery(input) {
|
|
4345
|
+
const trimmed = input.trim();
|
|
4346
|
+
if (!trimmed) {
|
|
4347
|
+
return {};
|
|
4348
|
+
}
|
|
4349
|
+
if (trimmed.startsWith("{")) {
|
|
4350
|
+
return JSON.parse(trimmed);
|
|
4351
|
+
}
|
|
4352
|
+
const result = {};
|
|
4353
|
+
for (const clause of trimmed.split("&")) {
|
|
4354
|
+
if (!clause) {
|
|
4355
|
+
continue;
|
|
4356
|
+
}
|
|
4357
|
+
const eq = clause.indexOf("=");
|
|
4358
|
+
if (eq === -1) {
|
|
4359
|
+
continue;
|
|
4360
|
+
}
|
|
4361
|
+
const path = clause.slice(0, eq);
|
|
4362
|
+
const value = clause.slice(eq + 1);
|
|
4363
|
+
const keys = [...path.matchAll(/\[([^\]]+)\]/g)].map((match) => match[1]);
|
|
4364
|
+
if (keys.length < 2) {
|
|
4365
|
+
continue;
|
|
4366
|
+
}
|
|
4367
|
+
const [field, operation] = keys;
|
|
4368
|
+
result[field] = { ...result[field], [operation]: value };
|
|
4369
|
+
}
|
|
4370
|
+
return result;
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4293
4373
|
const PIPELINE_BACKPRESSURE_MULTIPLIER = 2;
|
|
4294
4374
|
const DEFAULT_PIPELINE_BACKPRESSURE = 12;
|
|
4295
4375
|
function createPipelineBackpressureLock(limit) {
|
|
@@ -4313,16 +4393,13 @@ const ERROR_CODES = {
|
|
|
4313
4393
|
async function* storiesIterator(spaceId, params, onTotal) {
|
|
4314
4394
|
try {
|
|
4315
4395
|
let perPage = 500;
|
|
4316
|
-
const
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
transformedParams.contain_component = params.componentName;
|
|
4321
|
-
delete transformedParams.componentName;
|
|
4396
|
+
const { componentName, query, ...rest } = params ?? {};
|
|
4397
|
+
const transformedParams = { ...rest };
|
|
4398
|
+
if (componentName) {
|
|
4399
|
+
transformedParams.contain_component = componentName;
|
|
4322
4400
|
}
|
|
4323
|
-
if (
|
|
4324
|
-
transformedParams.filter_query =
|
|
4325
|
-
delete transformedParams.query;
|
|
4401
|
+
if (query) {
|
|
4402
|
+
transformedParams.filter_query = parseFilterQuery(query);
|
|
4326
4403
|
}
|
|
4327
4404
|
const result = await fetchStories(spaceId, {
|
|
4328
4405
|
...transformedParams,
|
|
@@ -4647,8 +4724,8 @@ class MigrationStream extends Transform {
|
|
|
4647
4724
|
id: story.id,
|
|
4648
4725
|
name: story.name || "",
|
|
4649
4726
|
content: story.content,
|
|
4650
|
-
published: story.published,
|
|
4651
|
-
unpublished_changes: story.unpublished_changes
|
|
4727
|
+
published: story.published ?? void 0,
|
|
4728
|
+
unpublished_changes: story.unpublished_changes ?? void 0
|
|
4652
4729
|
},
|
|
4653
4730
|
migrationTimestamp: this.timestamp,
|
|
4654
4731
|
migrationNames
|
|
@@ -4664,8 +4741,8 @@ class MigrationStream extends Transform {
|
|
|
4664
4741
|
storyId: story.id,
|
|
4665
4742
|
name: story.name,
|
|
4666
4743
|
content: storyContent,
|
|
4667
|
-
published: story.published,
|
|
4668
|
-
unpublished_changes: story.unpublished_changes
|
|
4744
|
+
published: story.published ?? void 0,
|
|
4745
|
+
unpublished_changes: story.unpublished_changes ?? void 0
|
|
4669
4746
|
};
|
|
4670
4747
|
} else if (processed && !contentChanged) {
|
|
4671
4748
|
this.results.skipped.push({
|
|
@@ -4807,7 +4884,6 @@ class UpdateStream extends Writable {
|
|
|
4807
4884
|
const payload = {
|
|
4808
4885
|
story: {
|
|
4809
4886
|
content,
|
|
4810
|
-
id: storyId,
|
|
4811
4887
|
name: storyName
|
|
4812
4888
|
},
|
|
4813
4889
|
force_update: "1"
|
|
@@ -5056,7 +5132,6 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
|
|
|
5056
5132
|
const payload = {
|
|
5057
5133
|
story: {
|
|
5058
5134
|
content: story.content,
|
|
5059
|
-
id: story.storyId,
|
|
5060
5135
|
name: story.name
|
|
5061
5136
|
},
|
|
5062
5137
|
force_update: "1"
|
|
@@ -5096,8 +5171,8 @@ rollbackCmd.action(async (migrationFile, _options, command) => {
|
|
|
5096
5171
|
}
|
|
5097
5172
|
});
|
|
5098
5173
|
|
|
5099
|
-
const program$
|
|
5100
|
-
const typesCommand = program$
|
|
5174
|
+
const program$8 = getProgram();
|
|
5175
|
+
const typesCommand = program$8.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`);
|
|
5101
5176
|
|
|
5102
5177
|
const getAssetJSONSchema = (title) => ({
|
|
5103
5178
|
$id: "#/asset",
|
|
@@ -5708,7 +5783,7 @@ const getPropertyTypeAnnotation = (property, prefix, suffix) => {
|
|
|
5708
5783
|
}
|
|
5709
5784
|
};
|
|
5710
5785
|
function getStoryType(property, prefix, suffix) {
|
|
5711
|
-
return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase(property))}${suffix ?? ""}>`;
|
|
5786
|
+
return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase$1(property))}${suffix ?? ""}>`;
|
|
5712
5787
|
}
|
|
5713
5788
|
const getComponentType = (componentName, options) => {
|
|
5714
5789
|
const prefix = options.typePrefix ?? "";
|
|
@@ -6099,7 +6174,7 @@ const deleteDatasourceEntry = async (spaceId, entryId) => {
|
|
|
6099
6174
|
handleAPIError("delete_datasource_entry", error, `Failed to delete datasource entry ${entryId}`);
|
|
6100
6175
|
}
|
|
6101
6176
|
};
|
|
6102
|
-
function isDatasource(item) {
|
|
6177
|
+
function isDatasource$1(item) {
|
|
6103
6178
|
return typeof item === "object" && item !== null && "slug" in item && typeof item.slug === "string";
|
|
6104
6179
|
}
|
|
6105
6180
|
const readDatasourcesFiles = async (options) => {
|
|
@@ -6132,7 +6207,7 @@ const readDatasourcesFiles = async (options) => {
|
|
|
6132
6207
|
continue;
|
|
6133
6208
|
}
|
|
6134
6209
|
for (const item of data) {
|
|
6135
|
-
if (isDatasource(item)) {
|
|
6210
|
+
if (isDatasource$1(item)) {
|
|
6136
6211
|
const existing = datasourceMap.get(item.slug);
|
|
6137
6212
|
if (existing) {
|
|
6138
6213
|
duplicates.push(`Datasource "${item.slug}" found in both "${existing.file}" and "${file}"`);
|
|
@@ -6232,8 +6307,8 @@ generateCmd.action(async (options, command) => {
|
|
|
6232
6307
|
}
|
|
6233
6308
|
});
|
|
6234
6309
|
|
|
6235
|
-
const program$
|
|
6236
|
-
const datasourcesCommand = program$
|
|
6310
|
+
const program$7 = getProgram();
|
|
6311
|
+
const datasourcesCommand = program$7.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`);
|
|
6237
6312
|
|
|
6238
6313
|
const DEFAULT_DATASOURCES_FILENAME = "datasources";
|
|
6239
6314
|
|
|
@@ -6722,7 +6797,62 @@ ${Object.entries(storyblokVars).map(([key, value]) => `${key}=${value}`).join("\
|
|
|
6722
6797
|
throw new Error(`Failed to create .env file: ${error.message}`);
|
|
6723
6798
|
}
|
|
6724
6799
|
};
|
|
6725
|
-
async
|
|
6800
|
+
const updateAngularEnvironmentFiles = async (projectPath, token, region) => {
|
|
6801
|
+
const environmentsDir = join(projectPath, "src", "environments");
|
|
6802
|
+
const envFiles = ["environment.ts", "environment.development.ts"];
|
|
6803
|
+
const updatedFiles = [];
|
|
6804
|
+
for (const envFile of envFiles) {
|
|
6805
|
+
const filePath = join(environmentsDir, envFile);
|
|
6806
|
+
try {
|
|
6807
|
+
let content = await fs.readFile(filePath, "utf-8");
|
|
6808
|
+
if (token) {
|
|
6809
|
+
content = content.replaceAll("STORYBLOK_DELIVERY_API_TOKEN", token);
|
|
6810
|
+
}
|
|
6811
|
+
if (region) {
|
|
6812
|
+
content = content.replaceAll("STORYBLOK_REGION", region);
|
|
6813
|
+
}
|
|
6814
|
+
await saveToFile(filePath, content);
|
|
6815
|
+
updatedFiles.push(filePath);
|
|
6816
|
+
} catch (error) {
|
|
6817
|
+
const fsError = error;
|
|
6818
|
+
if (fsError.code === "ENOENT") {
|
|
6819
|
+
continue;
|
|
6820
|
+
}
|
|
6821
|
+
throw new Error(`Failed to update ${envFile}: ${error.message}`);
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
return { updatedFiles };
|
|
6825
|
+
};
|
|
6826
|
+
async function handleEnvFileCreation(resolvedPath, token, region, template) {
|
|
6827
|
+
if (template === "angular") {
|
|
6828
|
+
if (!token && !region) {
|
|
6829
|
+
ui$1.info("No environment variables to write");
|
|
6830
|
+
return true;
|
|
6831
|
+
}
|
|
6832
|
+
try {
|
|
6833
|
+
const { updatedFiles } = await updateAngularEnvironmentFiles(resolvedPath, token, region);
|
|
6834
|
+
if (updatedFiles.length === 0) {
|
|
6835
|
+
ui$1.info("No Angular environment files found to update");
|
|
6836
|
+
return true;
|
|
6837
|
+
}
|
|
6838
|
+
const writtenVars = [token && "accessToken", region && "region"].filter(Boolean).join(", ");
|
|
6839
|
+
ui$1.ok(`Updated Angular environment files with: ${writtenVars}`, true);
|
|
6840
|
+
return true;
|
|
6841
|
+
} catch (error) {
|
|
6842
|
+
ui$1.warn(`Failed to update Angular environment files: ${error.message}`);
|
|
6843
|
+
if (token) {
|
|
6844
|
+
ui$1.info(
|
|
6845
|
+
`You can manually add accessToken to src/environments/environment.ts and src/environments/environment.development.ts`
|
|
6846
|
+
);
|
|
6847
|
+
}
|
|
6848
|
+
if (region) {
|
|
6849
|
+
ui$1.info(
|
|
6850
|
+
`You can manually add region to src/environments/environment.ts and src/environments/environment.development.ts`
|
|
6851
|
+
);
|
|
6852
|
+
}
|
|
6853
|
+
return false;
|
|
6854
|
+
}
|
|
6855
|
+
}
|
|
6726
6856
|
const envVars = {};
|
|
6727
6857
|
if (token) {
|
|
6728
6858
|
envVars.STORYBLOK_DELIVERY_API_TOKEN = token;
|
|
@@ -6844,13 +6974,13 @@ async function promptForLogin(verbose) {
|
|
|
6844
6974
|
return null;
|
|
6845
6975
|
}
|
|
6846
6976
|
}
|
|
6847
|
-
const program$
|
|
6848
|
-
program$
|
|
6977
|
+
const program$6 = getProgram();
|
|
6978
|
+
program$6.command(`${commands.CREATE} [project-path]`).alias("c").description(`Scaffold a new project using Storyblok`).option("-t, --template <template>", "technology starter template").option("-b, --blueprint <blueprint>", "[DEPRECATED] use --template instead").option("--skip-space", "skip space creation").option("--token <token>", "Storyblok access token (skip space creation and use this token)").option(
|
|
6849
6979
|
"-r, --region <region>",
|
|
6850
6980
|
`The region to apply to the generated project template (does not affect space creation).`
|
|
6851
6981
|
).action(async (projectPath, options) => {
|
|
6852
6982
|
ui.title(`${commands.CREATE}`, colorPalette.CREATE);
|
|
6853
|
-
const verbose = program$
|
|
6983
|
+
const verbose = program$6.opts().verbose;
|
|
6854
6984
|
const { template, blueprint, token } = options;
|
|
6855
6985
|
if (options.region && !isRegion(options.region)) {
|
|
6856
6986
|
handleError(new CommandError(`The provided region: ${options.region} is not valid. Please use one of the following values: ${Object.values(regions).join(" | ")}`));
|
|
@@ -6954,13 +7084,13 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
6954
7084
|
let userData;
|
|
6955
7085
|
let whereToCreateSpace = "personal";
|
|
6956
7086
|
if (token) {
|
|
6957
|
-
await handleEnvFileCreation(resolvedPath, token, options.region || region);
|
|
7087
|
+
await handleEnvFileCreation(resolvedPath, token, options.region || region, technologyTemplate);
|
|
6958
7088
|
showNextSteps(technologyTemplate, finalProjectPath);
|
|
6959
7089
|
return;
|
|
6960
7090
|
}
|
|
6961
7091
|
if (options.skipSpace) {
|
|
6962
7092
|
if (options.region || region) {
|
|
6963
|
-
await handleEnvFileCreation(resolvedPath, void 0, options.region || region);
|
|
7093
|
+
await handleEnvFileCreation(resolvedPath, void 0, options.region || region, technologyTemplate);
|
|
6964
7094
|
}
|
|
6965
7095
|
showNextSteps(technologyTemplate, finalProjectPath);
|
|
6966
7096
|
return;
|
|
@@ -7023,16 +7153,16 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
7023
7153
|
name: toHumanReadable(projectName),
|
|
7024
7154
|
domain: blueprintDomain
|
|
7025
7155
|
};
|
|
7156
|
+
const createSpaceQuery = {};
|
|
7026
7157
|
if (whereToCreateSpace === "org") {
|
|
7027
|
-
|
|
7028
|
-
spaceToCreate.in_org = true;
|
|
7158
|
+
createSpaceQuery.in_org = true;
|
|
7029
7159
|
} else if (whereToCreateSpace === "partner") {
|
|
7030
|
-
|
|
7160
|
+
createSpaceQuery.assign_partner = true;
|
|
7031
7161
|
}
|
|
7032
|
-
createdSpace = await createSpace(spaceToCreate);
|
|
7162
|
+
createdSpace = await createSpace(spaceToCreate, createSpaceQuery);
|
|
7033
7163
|
spinnerSpace.succeed(`Space "${chalk.hex(colorPalette.PRIMARY)(toHumanReadable(projectName))}" created successfully`);
|
|
7034
7164
|
if (createdSpace?.first_token) {
|
|
7035
|
-
await handleEnvFileCreation(resolvedPath, createdSpace.first_token, region);
|
|
7165
|
+
await handleEnvFileCreation(resolvedPath, createdSpace.first_token, region, technologyTemplate);
|
|
7036
7166
|
}
|
|
7037
7167
|
if (createdSpace?.id) {
|
|
7038
7168
|
try {
|
|
@@ -7069,8 +7199,8 @@ program$5.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
|
|
|
7069
7199
|
ui.br();
|
|
7070
7200
|
});
|
|
7071
7201
|
|
|
7072
|
-
const program$
|
|
7073
|
-
const logsCommand = program$
|
|
7202
|
+
const program$5 = getProgram();
|
|
7203
|
+
const logsCommand = program$5.command(commands.LOGS).alias("lg").description(`Inspect and manage logs.`);
|
|
7074
7204
|
|
|
7075
7205
|
const listCmd$1 = logsCommand.command("list").description("List logs").option("-s, --space <space>", "space ID");
|
|
7076
7206
|
listCmd$1.action(async (_options, command) => {
|
|
@@ -7095,8 +7225,8 @@ pruneCmd$1.action(async (options, command) => {
|
|
|
7095
7225
|
ui.info(`Deleted ${deletedFilesCount} log file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
7096
7226
|
});
|
|
7097
7227
|
|
|
7098
|
-
const program$
|
|
7099
|
-
const reportsCommand = program$
|
|
7228
|
+
const program$4 = getProgram();
|
|
7229
|
+
const reportsCommand = program$4.command(commands.REPORTS).alias("rp").description("Inspect and manage reports.");
|
|
7100
7230
|
|
|
7101
7231
|
const listCmd = reportsCommand.command("list").description("List reports").option("-s, --space <space>", "space ID");
|
|
7102
7232
|
listCmd.action(async (_options, command) => {
|
|
@@ -7121,8 +7251,8 @@ pruneCmd.action(async (options, command) => {
|
|
|
7121
7251
|
ui.info(`Deleted ${deletedFilesCount} report file${deletedFilesCount === 1 ? "" : "s"}`);
|
|
7122
7252
|
});
|
|
7123
7253
|
|
|
7124
|
-
const program$
|
|
7125
|
-
const assetsCommand = program$
|
|
7254
|
+
const program$3 = getProgram();
|
|
7255
|
+
const assetsCommand = program$3.command(commands.ASSETS).description(`Manage your space's assets`);
|
|
7126
7256
|
|
|
7127
7257
|
const fetchAssets = async ({ spaceId, params }) => {
|
|
7128
7258
|
try {
|
|
@@ -7170,7 +7300,7 @@ const createAssetInternalTag = async (spaceId, name) => {
|
|
|
7170
7300
|
const client = getMapiClient();
|
|
7171
7301
|
const { data } = await client.internalTags.create({
|
|
7172
7302
|
path: { space_id: Number(spaceId) },
|
|
7173
|
-
body: { name, object_type: "asset" },
|
|
7303
|
+
body: { internal_tag: { name, object_type: "asset" } },
|
|
7174
7304
|
throwOnError: true
|
|
7175
7305
|
});
|
|
7176
7306
|
const tag = data?.internal_tag;
|
|
@@ -7294,7 +7424,7 @@ const updateAsset = async (id, asset, { spaceId, fileBuffer }) => {
|
|
|
7294
7424
|
const createAsset = async (asset, fileBuffer, { spaceId }) => {
|
|
7295
7425
|
try {
|
|
7296
7426
|
const client = getMapiClient();
|
|
7297
|
-
const { id: _id, ...assetBody } = asset;
|
|
7427
|
+
const { id: _id, filename: _filename, ...assetBody } = asset;
|
|
7298
7428
|
return await client.assets.create({
|
|
7299
7429
|
body: assetBody,
|
|
7300
7430
|
file: fileBuffer,
|
|
@@ -7354,6 +7484,26 @@ const parseAssetData = (raw) => {
|
|
|
7354
7484
|
throw new Error(`Invalid --data JSON: ${toError(maybeError).message}`);
|
|
7355
7485
|
}
|
|
7356
7486
|
};
|
|
7487
|
+
const toAssetUpload = (partial, shortFilename) => {
|
|
7488
|
+
const nullToUndef = (value) => value ?? void 0;
|
|
7489
|
+
return {
|
|
7490
|
+
id: partial.id,
|
|
7491
|
+
filename: partial.filename,
|
|
7492
|
+
short_filename: shortFilename,
|
|
7493
|
+
asset_folder_id: nullToUndef(partial.asset_folder_id),
|
|
7494
|
+
ext_id: nullToUndef(partial.ext_id),
|
|
7495
|
+
alt: nullToUndef(partial.alt),
|
|
7496
|
+
copyright: nullToUndef(partial.copyright),
|
|
7497
|
+
title: nullToUndef(partial.title),
|
|
7498
|
+
source: nullToUndef(partial.source),
|
|
7499
|
+
expire_at: nullToUndef(partial.expire_at),
|
|
7500
|
+
publish_at: nullToUndef(partial.publish_at),
|
|
7501
|
+
focus: nullToUndef(partial.focus),
|
|
7502
|
+
is_private: nullToUndef(partial.is_private),
|
|
7503
|
+
internal_tag_ids: partial.internal_tag_ids,
|
|
7504
|
+
meta_data: nullToUndef(partial.meta_data)
|
|
7505
|
+
};
|
|
7506
|
+
};
|
|
7357
7507
|
const getSidecarFilename = (assetBinaryPath) => {
|
|
7358
7508
|
return join(dirname(assetBinaryPath), `${basename(assetBinaryPath, extname(assetBinaryPath))}.json`);
|
|
7359
7509
|
};
|
|
@@ -7382,7 +7532,7 @@ const isRemoteSource = (assetBinaryPath) => {
|
|
|
7382
7532
|
return false;
|
|
7383
7533
|
}
|
|
7384
7534
|
};
|
|
7385
|
-
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.
|
|
7535
|
+
const isValidManifestEntry = (entry) => Boolean(typeof entry.old_id === "number" && typeof entry.new_id === "number" && entry.new_filename);
|
|
7386
7536
|
const loadAssetMap = async (manifestFile) => {
|
|
7387
7537
|
const manifest = await loadManifest(manifestFile);
|
|
7388
7538
|
const entries = manifest.filter(isValidManifestEntry).map((e) => [
|
|
@@ -7762,8 +7912,10 @@ const readLocalAssetsStream = ({
|
|
|
7762
7912
|
const sidecar = await loadSidecarAssetData(binaryFilePath);
|
|
7763
7913
|
const shortFilename = sidecar.short_filename || (sidecar.filename ? basename(sidecar.filename) : void 0) || file;
|
|
7764
7914
|
const asset = {
|
|
7765
|
-
...sidecar,
|
|
7766
|
-
|
|
7915
|
+
...toAssetUpload(sidecar, shortFilename),
|
|
7916
|
+
// Carry the read-only tag detail for source→target tag-name
|
|
7917
|
+
// translation in `processAsset`; it is stripped before the API call.
|
|
7918
|
+
...sidecar.internal_tags_list ? { internal_tags_list: sidecar.internal_tags_list } : {}
|
|
7767
7919
|
};
|
|
7768
7920
|
const fileBuffer = await readFile$1(binaryFilePath);
|
|
7769
7921
|
const sidecarPath = getSidecarFilename(binaryFilePath);
|
|
@@ -7847,7 +7999,7 @@ const mapInternalTagIds = (sourceIds, sourceTags, assetInternalTagsByName, onUnm
|
|
|
7847
7999
|
const sourceName = sourceNamesById.get(sourceId);
|
|
7848
8000
|
const targetId = typeof sourceName === "string" ? assetInternalTagsByName.get(sourceName) : void 0;
|
|
7849
8001
|
if (typeof targetId === "number") {
|
|
7850
|
-
mapped.push(
|
|
8002
|
+
mapped.push(targetId);
|
|
7851
8003
|
} else {
|
|
7852
8004
|
onUnmappedTag?.({ sourceId, name: sourceName });
|
|
7853
8005
|
}
|
|
@@ -7899,10 +8051,11 @@ const processAsset = async ({
|
|
|
7899
8051
|
const remoteAssetId = hasId(localAsset) ? maps.assets.get(localAsset.id)?.new.id || localAsset.id : void 0;
|
|
7900
8052
|
const remoteAsset = remoteAssetId ? await transports.getAsset(remoteAssetId) : null;
|
|
7901
8053
|
const sourceTags = localAsset.internal_tags_list;
|
|
7902
|
-
const resolveInternalTagIds = (sourceIds) => maps.assetInternalTagsByName ? mapInternalTagIds(sourceIds, sourceTags, maps.assetInternalTagsByName, onUnmappedTag) : (sourceIds ?? []).map((id) =>
|
|
8054
|
+
const resolveInternalTagIds = (sourceIds) => maps.assetInternalTagsByName ? mapInternalTagIds(sourceIds, sourceTags, maps.assetInternalTagsByName, onUnmappedTag) : (sourceIds ?? []).map((id) => Number(id));
|
|
7903
8055
|
let newRemoteAsset;
|
|
7904
8056
|
let status;
|
|
7905
8057
|
if (remoteAsset) {
|
|
8058
|
+
const nullToUndef = (v) => v ?? void 0;
|
|
7906
8059
|
const updatePayload = {
|
|
7907
8060
|
asset_folder_id: remoteFolderId,
|
|
7908
8061
|
alt: "alt" in localAsset ? localAsset.alt : remoteAsset.alt,
|
|
@@ -7911,24 +8064,28 @@ const processAsset = async ({
|
|
|
7911
8064
|
source: "source" in localAsset ? localAsset.source : remoteAsset.source,
|
|
7912
8065
|
is_private: "is_private" in localAsset ? localAsset.is_private : remoteAsset.is_private,
|
|
7913
8066
|
focus: "focus" in localAsset ? localAsset.focus : remoteAsset.focus,
|
|
7914
|
-
expire_at: "expire_at" in localAsset ? localAsset.expire_at : remoteAsset.expire_at,
|
|
7915
|
-
publish_at: "publish_at" in localAsset ? localAsset.publish_at : remoteAsset.publish_at,
|
|
7916
|
-
internal_tag_ids:
|
|
7917
|
-
meta_data: "meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data
|
|
8067
|
+
expire_at: nullToUndef("expire_at" in localAsset ? localAsset.expire_at : remoteAsset.expire_at),
|
|
8068
|
+
publish_at: nullToUndef("publish_at" in localAsset ? localAsset.publish_at : remoteAsset.publish_at),
|
|
8069
|
+
internal_tag_ids: localAsset.internal_tag_ids ? resolveInternalTagIds(localAsset.internal_tag_ids) : remoteAsset.internal_tag_ids,
|
|
8070
|
+
meta_data: nullToUndef("meta_data" in localAsset ? localAsset.meta_data : remoteAsset.meta_data)
|
|
7918
8071
|
};
|
|
7919
8072
|
await transports.updateAsset(
|
|
7920
8073
|
remoteAsset.id,
|
|
7921
8074
|
{ ...updatePayload, short_filename: remoteAsset.short_filename },
|
|
7922
8075
|
fileBuffer
|
|
7923
8076
|
);
|
|
7924
|
-
newRemoteAsset = {
|
|
8077
|
+
newRemoteAsset = {
|
|
8078
|
+
...remoteAsset,
|
|
8079
|
+
...updatePayload,
|
|
8080
|
+
is_private: updatePayload.is_private ?? remoteAsset.is_private
|
|
8081
|
+
};
|
|
7925
8082
|
status = "updated";
|
|
7926
8083
|
} else if (hasShortFilename(localAsset)) {
|
|
7927
|
-
const
|
|
7928
|
-
const
|
|
8084
|
+
const mappedTagIds = localAsset.internal_tag_ids ? resolveInternalTagIds(localAsset.internal_tag_ids) : void 0;
|
|
8085
|
+
const { internal_tag_ids: _sourceTagIds, ...uploadBase } = toAssetUpload(localAsset, localAsset.short_filename);
|
|
7929
8086
|
const createPayload = {
|
|
7930
|
-
...
|
|
7931
|
-
asset_folder_id: remoteFolderId,
|
|
8087
|
+
...uploadBase,
|
|
8088
|
+
asset_folder_id: remoteFolderId ?? void 0,
|
|
7932
8089
|
...mappedTagIds !== void 0 ? { internal_tag_ids: mappedTagIds } : {}
|
|
7933
8090
|
};
|
|
7934
8091
|
newRemoteAsset = await transports.createAsset(createPayload, fileBuffer);
|
|
@@ -8161,8 +8318,8 @@ const traverseAndMapBySchema = (data, {
|
|
|
8161
8318
|
const dataNew = { ...data };
|
|
8162
8319
|
for (const [fieldName, fieldValue] of Object.entries(data)) {
|
|
8163
8320
|
const fieldSchema = schema[fieldName.replace(/__i18n__.*/, "")];
|
|
8164
|
-
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema
|
|
8165
|
-
const fieldRefMapper = typeof fieldType === "string"
|
|
8321
|
+
const fieldType = fieldSchema && typeof fieldSchema === "object" && "type" in fieldSchema ? fieldSchema.type : void 0;
|
|
8322
|
+
const fieldRefMapper = typeof fieldType === "string" ? fieldRefMappers2[fieldType] : void 0;
|
|
8166
8323
|
if (fieldRefMapper) {
|
|
8167
8324
|
dataNew[fieldName] = fieldRefMapper(fieldValue, {
|
|
8168
8325
|
schema: fieldSchema,
|
|
@@ -8227,6 +8384,9 @@ const richtextFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRef
|
|
|
8227
8384
|
fieldRefMappers: fieldRefMappers2
|
|
8228
8385
|
});
|
|
8229
8386
|
const multilinkFieldRefMapper = (data, { maps }) => {
|
|
8387
|
+
if (!data || typeof data !== "object") {
|
|
8388
|
+
return data;
|
|
8389
|
+
}
|
|
8230
8390
|
if (data.linktype !== "story") {
|
|
8231
8391
|
return data;
|
|
8232
8392
|
}
|
|
@@ -8246,6 +8406,9 @@ const bloksFieldRefMapper = (data, { schemas, maps, fieldRefMappers: fieldRefMap
|
|
|
8246
8406
|
}));
|
|
8247
8407
|
};
|
|
8248
8408
|
const assetFieldRefMapper = (data, { maps }) => {
|
|
8409
|
+
if (!data || typeof data !== "object") {
|
|
8410
|
+
return data;
|
|
8411
|
+
}
|
|
8249
8412
|
const mappedAsset = typeof data.id === "number" ? maps.assets?.get(data.id) : void 0;
|
|
8250
8413
|
if (!mappedAsset) {
|
|
8251
8414
|
return data;
|
|
@@ -8263,7 +8426,7 @@ const multiassetFieldRefMapper = (data, options) => {
|
|
|
8263
8426
|
return data.map((d) => assetFieldRefMapper(d, options));
|
|
8264
8427
|
};
|
|
8265
8428
|
const optionsFieldRefMapper = (data, { schema, maps }) => {
|
|
8266
|
-
if (schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
8429
|
+
if (!schema || !("source" in schema) || schema.source !== "internal_stories" || !Array.isArray(data)) {
|
|
8267
8430
|
return data;
|
|
8268
8431
|
}
|
|
8269
8432
|
return data.map((d) => maps.stories?.get(d) || d);
|
|
@@ -9112,11 +9275,7 @@ pushCmd$1.action(async (assetInput, options, command) => {
|
|
|
9112
9275
|
const sourceBasename = isRemoteSource(assetBinaryPath) ? basename(new URL(assetBinaryPath).pathname) : basename(assetBinaryPath);
|
|
9113
9276
|
const shortFilename = options.shortFilename || assetDataPartial.short_filename || sourceBasename;
|
|
9114
9277
|
const folderId = options.folder ? Number(options.folder) : void 0;
|
|
9115
|
-
assetData = {
|
|
9116
|
-
...assetDataPartial,
|
|
9117
|
-
short_filename: shortFilename,
|
|
9118
|
-
asset_folder_id: folderId
|
|
9119
|
-
};
|
|
9278
|
+
assetData = { ...toAssetUpload(assetDataPartial, shortFilename), asset_folder_id: folderId };
|
|
9120
9279
|
}
|
|
9121
9280
|
const getAssetTransport = makeGetAssetAPITransport({ spaceId: targetSpace });
|
|
9122
9281
|
const createAssetTransport = options.dryRun ? async (asset) => asset : makeCreateAssetAPITransport({ spaceId: targetSpace });
|
|
@@ -9276,8 +9435,8 @@ transferCmd.action(async (assetIds, options, command) => {
|
|
|
9276
9435
|
process.exitCode = summary.failed > 0 ? 1 : 0;
|
|
9277
9436
|
});
|
|
9278
9437
|
|
|
9279
|
-
const program$
|
|
9280
|
-
const storiesCommand = program$
|
|
9438
|
+
const program$2 = getProgram();
|
|
9439
|
+
const storiesCommand = program$2.command(commands.STORIES).description(`Manage your space's stories`);
|
|
9281
9440
|
|
|
9282
9441
|
const pullCmd = storiesCommand.command("pull").option("-s, --space <space>", "space ID").option("-d, --dry-run", "Preview changes without applying them to Storyblok").option("-q, --query <query>", 'Filter stories by content attributes using Storyblok filter query syntax. Example: --query="[highlighted][in]=true"').option("--starts-with <path>", 'Filter stories by path. Example: --starts-with="/en/blog/"').description(`Download your space's stories as separate json files.`);
|
|
9283
9442
|
pullCmd.action(async (options, command) => {
|
|
@@ -9313,7 +9472,7 @@ pullCmd.action(async (options, command) => {
|
|
|
9313
9472
|
fetchStoriesStream({
|
|
9314
9473
|
spaceId: space,
|
|
9315
9474
|
params: {
|
|
9316
|
-
filter_query: options.query,
|
|
9475
|
+
filter_query: options.query ? parseFilterQuery(options.query) : void 0,
|
|
9317
9476
|
starts_with: options.startsWith
|
|
9318
9477
|
},
|
|
9319
9478
|
setTotalPages: (totalPages) => {
|
|
@@ -9782,6 +9941,1747 @@ pushCmd.action(async (options, command) => {
|
|
|
9782
9941
|
}
|
|
9783
9942
|
});
|
|
9784
9943
|
|
|
9944
|
+
const program$1 = getProgram();
|
|
9945
|
+
const schemaCommand = program$1.command(commands.SCHEMA).description(`Manage your space's schema from code`);
|
|
9946
|
+
|
|
9947
|
+
function isRecord(value) {
|
|
9948
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9949
|
+
}
|
|
9950
|
+
const COMPONENT_STRIP_KEYS = /* @__PURE__ */ new Set([
|
|
9951
|
+
"id",
|
|
9952
|
+
"created_at",
|
|
9953
|
+
"updated_at",
|
|
9954
|
+
"real_name",
|
|
9955
|
+
// API-computed display/technical name, read-only
|
|
9956
|
+
"preset_id",
|
|
9957
|
+
// Instance-level preset selection, not part of schema definition
|
|
9958
|
+
"all_presets",
|
|
9959
|
+
// Computed list of presets, managed via /presets API
|
|
9960
|
+
"internal_tags_list",
|
|
9961
|
+
// Read-only expanded form of internal_tag_ids ({id, name} objects)
|
|
9962
|
+
"content_type_asset_preview",
|
|
9963
|
+
// Read-only, not in ComponentCreate/ComponentUpdate
|
|
9964
|
+
"image",
|
|
9965
|
+
// Read-only preview image URL
|
|
9966
|
+
"preview_tmpl",
|
|
9967
|
+
// Read-only preview template
|
|
9968
|
+
"metadata",
|
|
9969
|
+
// Not in current API types, stripped defensively
|
|
9970
|
+
"component_group_uuid"
|
|
9971
|
+
// UI grouping; stripped by default — kept for diffing only when a block opts into the group escape hatch
|
|
9972
|
+
]);
|
|
9973
|
+
const DATASOURCE_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "created_at", "updated_at"]);
|
|
9974
|
+
const DATASOURCE_DIMENSION_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "datasource_id", "created_at", "updated_at"]);
|
|
9975
|
+
const DATASOURCE_DEFAULTS = {
|
|
9976
|
+
dimensions: []
|
|
9977
|
+
};
|
|
9978
|
+
const COMPONENT_DEFAULTS = {
|
|
9979
|
+
display_name: "",
|
|
9980
|
+
description: "",
|
|
9981
|
+
color: "",
|
|
9982
|
+
icon: "",
|
|
9983
|
+
preview_field: "",
|
|
9984
|
+
internal_tag_ids: []
|
|
9985
|
+
};
|
|
9986
|
+
function applyDefaults(entity, defaults) {
|
|
9987
|
+
const result = { ...entity };
|
|
9988
|
+
for (const [key, defaultValue] of Object.entries(defaults)) {
|
|
9989
|
+
if (result[key] === void 0 || result[key] === null) {
|
|
9990
|
+
Object.assign(result, { [key]: defaultValue });
|
|
9991
|
+
}
|
|
9992
|
+
}
|
|
9993
|
+
return result;
|
|
9994
|
+
}
|
|
9995
|
+
const INDENT = " ";
|
|
9996
|
+
function formatValue(value, depth) {
|
|
9997
|
+
const indent = INDENT.repeat(depth);
|
|
9998
|
+
const innerIndent = INDENT.repeat(depth + 1);
|
|
9999
|
+
if (value === null || value === void 0) {
|
|
10000
|
+
return String(value);
|
|
10001
|
+
}
|
|
10002
|
+
if (typeof value === "string") {
|
|
10003
|
+
return `'${value.replace(/'/g, "\\'")}'`;
|
|
10004
|
+
}
|
|
10005
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
10006
|
+
return String(value);
|
|
10007
|
+
}
|
|
10008
|
+
if (Array.isArray(value)) {
|
|
10009
|
+
if (value.length === 0) {
|
|
10010
|
+
return "[]";
|
|
10011
|
+
}
|
|
10012
|
+
const items = value.map((item) => `${innerIndent}${formatValue(item, depth + 1)},`);
|
|
10013
|
+
return `[
|
|
10014
|
+
${items.join("\n")}
|
|
10015
|
+
${indent}]`;
|
|
10016
|
+
}
|
|
10017
|
+
if (typeof value === "object") {
|
|
10018
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0 && v !== null).sort(([a], [b]) => a.localeCompare(b));
|
|
10019
|
+
if (entries.length === 0) {
|
|
10020
|
+
return "{}";
|
|
10021
|
+
}
|
|
10022
|
+
const props = entries.map(
|
|
10023
|
+
([key, val]) => `${innerIndent}${key}: ${formatValue(val, depth + 1)},`
|
|
10024
|
+
);
|
|
10025
|
+
return `{
|
|
10026
|
+
${props.join("\n")}
|
|
10027
|
+
${indent}}`;
|
|
10028
|
+
}
|
|
10029
|
+
return String(value);
|
|
10030
|
+
}
|
|
10031
|
+
function fileTimestamp(iso) {
|
|
10032
|
+
return iso.replace(/\D/g, "").slice(0, 14);
|
|
10033
|
+
}
|
|
10034
|
+
function displayPath(filePath, userPath) {
|
|
10035
|
+
return userPath && isAbsolute(userPath) ? filePath : relative(process.cwd(), filePath);
|
|
10036
|
+
}
|
|
10037
|
+
function stripKeys(obj, keysToStrip) {
|
|
10038
|
+
const result = {};
|
|
10039
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
10040
|
+
if (!keysToStrip.has(key) && value !== void 0 && value !== null) {
|
|
10041
|
+
result[key] = value;
|
|
10042
|
+
}
|
|
10043
|
+
}
|
|
10044
|
+
return result;
|
|
10045
|
+
}
|
|
10046
|
+
|
|
10047
|
+
function mapFieldToWire(field) {
|
|
10048
|
+
const { name, allow, datasource, ...rest } = field;
|
|
10049
|
+
const value = { ...rest };
|
|
10050
|
+
if (allow !== void 0) {
|
|
10051
|
+
value.component_whitelist = allow;
|
|
10052
|
+
}
|
|
10053
|
+
if (datasource !== void 0) {
|
|
10054
|
+
value.datasource_slug = datasource;
|
|
10055
|
+
}
|
|
10056
|
+
return { name: typeof name === "string" ? name : "", value };
|
|
10057
|
+
}
|
|
10058
|
+
function mapBlockToWire(block) {
|
|
10059
|
+
const { fields, ...rest } = block;
|
|
10060
|
+
const schema = {};
|
|
10061
|
+
if (Array.isArray(fields)) {
|
|
10062
|
+
for (const field of fields) {
|
|
10063
|
+
if (!isRecord(field)) {
|
|
10064
|
+
continue;
|
|
10065
|
+
}
|
|
10066
|
+
const { name, value } = mapFieldToWire(field);
|
|
10067
|
+
if (name) {
|
|
10068
|
+
schema[name] = value;
|
|
10069
|
+
}
|
|
10070
|
+
}
|
|
10071
|
+
}
|
|
10072
|
+
return { ...rest, schema };
|
|
10073
|
+
}
|
|
10074
|
+
function mapDatasourceToWire(datasource) {
|
|
10075
|
+
return datasource;
|
|
10076
|
+
}
|
|
10077
|
+
|
|
10078
|
+
function isComponent(value) {
|
|
10079
|
+
return isRecord(value) && typeof value.name === "string" && Array.isArray(value.fields);
|
|
10080
|
+
}
|
|
10081
|
+
function isDatasource(value) {
|
|
10082
|
+
return isRecord(value) && typeof value.name === "string" && typeof value.slug === "string" && !Array.isArray(value.fields);
|
|
10083
|
+
}
|
|
10084
|
+
function isSchemaObject(value) {
|
|
10085
|
+
return isRecord(value) && ("blocks" in value || "datasources" in value);
|
|
10086
|
+
}
|
|
10087
|
+
function emptySchemaData() {
|
|
10088
|
+
return { components: [], datasources: [] };
|
|
10089
|
+
}
|
|
10090
|
+
function classifyExports(moduleExports) {
|
|
10091
|
+
const data = emptySchemaData();
|
|
10092
|
+
const seenComponents = /* @__PURE__ */ new Set();
|
|
10093
|
+
const seenDatasources = /* @__PURE__ */ new Set();
|
|
10094
|
+
function collect(value) {
|
|
10095
|
+
if (isComponent(value)) {
|
|
10096
|
+
if (seenComponents.has(value.name)) {
|
|
10097
|
+
return;
|
|
10098
|
+
}
|
|
10099
|
+
seenComponents.add(value.name);
|
|
10100
|
+
data.components.push(mapBlockToWire(value));
|
|
10101
|
+
} else if (isDatasource(value)) {
|
|
10102
|
+
if (seenDatasources.has(value.name)) {
|
|
10103
|
+
return;
|
|
10104
|
+
}
|
|
10105
|
+
seenDatasources.add(value.name);
|
|
10106
|
+
data.datasources.push(mapDatasourceToWire(value));
|
|
10107
|
+
}
|
|
10108
|
+
}
|
|
10109
|
+
for (const value of Object.values(moduleExports)) {
|
|
10110
|
+
if (isSchemaObject(value)) {
|
|
10111
|
+
for (const group of Object.values(value)) {
|
|
10112
|
+
if (isRecord(group)) {
|
|
10113
|
+
for (const entity of Object.values(group)) {
|
|
10114
|
+
collect(entity);
|
|
10115
|
+
}
|
|
10116
|
+
}
|
|
10117
|
+
}
|
|
10118
|
+
} else {
|
|
10119
|
+
collect(value);
|
|
10120
|
+
}
|
|
10121
|
+
}
|
|
10122
|
+
return data;
|
|
10123
|
+
}
|
|
10124
|
+
async function loadSchema(entryPath) {
|
|
10125
|
+
const { createJiti } = await import('jiti');
|
|
10126
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
10127
|
+
const { resolve } = await import('pathe');
|
|
10128
|
+
const entryAbs = resolve(entryPath);
|
|
10129
|
+
const entryMod = await jiti.import(entryAbs);
|
|
10130
|
+
return classifyExports(entryMod);
|
|
10131
|
+
}
|
|
10132
|
+
|
|
10133
|
+
function sortSchemaByPos$1(schema) {
|
|
10134
|
+
const entries = Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
|
|
10135
|
+
const posA = typeof a.pos === "number" ? a.pos : Infinity;
|
|
10136
|
+
const posB = typeof b.pos === "number" ? b.pos : Infinity;
|
|
10137
|
+
return posA - posB;
|
|
10138
|
+
});
|
|
10139
|
+
return Object.fromEntries(
|
|
10140
|
+
entries.map(([key, field]) => {
|
|
10141
|
+
const { id, ...rest } = field;
|
|
10142
|
+
return [key, rest];
|
|
10143
|
+
})
|
|
10144
|
+
);
|
|
10145
|
+
}
|
|
10146
|
+
function serializeComponent(component, options = {}) {
|
|
10147
|
+
const stripSet = options.includeGroupUuid ? new Set([...COMPONENT_STRIP_KEYS].filter((key) => key !== "component_group_uuid")) : COMPONENT_STRIP_KEYS;
|
|
10148
|
+
const clean = stripKeys(component, stripSet);
|
|
10149
|
+
if (clean.schema && typeof clean.schema === "object") {
|
|
10150
|
+
clean.schema = sortSchemaByPos$1(clean.schema);
|
|
10151
|
+
}
|
|
10152
|
+
const ordered = {};
|
|
10153
|
+
if (clean.name !== void 0) {
|
|
10154
|
+
ordered.name = clean.name;
|
|
10155
|
+
}
|
|
10156
|
+
if (clean.display_name !== void 0) {
|
|
10157
|
+
ordered.display_name = clean.display_name;
|
|
10158
|
+
}
|
|
10159
|
+
if (clean.is_root !== void 0) {
|
|
10160
|
+
ordered.is_root = clean.is_root;
|
|
10161
|
+
}
|
|
10162
|
+
if (clean.is_nestable !== void 0) {
|
|
10163
|
+
ordered.is_nestable = clean.is_nestable;
|
|
10164
|
+
}
|
|
10165
|
+
const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
|
|
10166
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
10167
|
+
if (!handled.has(key)) {
|
|
10168
|
+
ordered[key] = value;
|
|
10169
|
+
}
|
|
10170
|
+
}
|
|
10171
|
+
if (clean.schema !== void 0) {
|
|
10172
|
+
ordered.schema = clean.schema;
|
|
10173
|
+
}
|
|
10174
|
+
return `defineBlock(${formatValue(ordered, 0)})`;
|
|
10175
|
+
}
|
|
10176
|
+
function serializeDatasource(datasource) {
|
|
10177
|
+
const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
|
|
10178
|
+
if (Array.isArray(clean.dimensions)) {
|
|
10179
|
+
clean.dimensions = clean.dimensions.map(
|
|
10180
|
+
(dim) => typeof dim === "object" && dim !== null ? stripKeys(dim, DATASOURCE_DIMENSION_STRIP_KEYS) : dim
|
|
10181
|
+
);
|
|
10182
|
+
}
|
|
10183
|
+
const ordered = {};
|
|
10184
|
+
if (clean.name !== void 0) {
|
|
10185
|
+
ordered.name = clean.name;
|
|
10186
|
+
}
|
|
10187
|
+
if (clean.slug !== void 0) {
|
|
10188
|
+
ordered.slug = clean.slug;
|
|
10189
|
+
}
|
|
10190
|
+
const handled = /* @__PURE__ */ new Set(["name", "slug"]);
|
|
10191
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
10192
|
+
if (!handled.has(key)) {
|
|
10193
|
+
ordered[key] = value;
|
|
10194
|
+
}
|
|
10195
|
+
}
|
|
10196
|
+
return `defineDatasource(${formatValue(ordered, 0)})`;
|
|
10197
|
+
}
|
|
10198
|
+
|
|
10199
|
+
function diffEntity(type, name, localSerialized, remoteSerialized) {
|
|
10200
|
+
if (!remoteSerialized && localSerialized) {
|
|
10201
|
+
return { type, name, action: "create", diff: null, local: null, remote: null };
|
|
10202
|
+
}
|
|
10203
|
+
if (remoteSerialized && !localSerialized) {
|
|
10204
|
+
return { type, name, action: "stale", diff: null, local: null, remote: null };
|
|
10205
|
+
}
|
|
10206
|
+
if (localSerialized === remoteSerialized) {
|
|
10207
|
+
return { type, name, action: "unchanged", diff: null, local: null, remote: null };
|
|
10208
|
+
}
|
|
10209
|
+
const patch = createTwoFilesPatch(
|
|
10210
|
+
`remote/${name}`,
|
|
10211
|
+
`local/${name}`,
|
|
10212
|
+
remoteSerialized,
|
|
10213
|
+
localSerialized,
|
|
10214
|
+
"remote",
|
|
10215
|
+
"local"
|
|
10216
|
+
);
|
|
10217
|
+
return { type, name, action: "update", diff: patch, local: null, remote: null };
|
|
10218
|
+
}
|
|
10219
|
+
function diffSchema(local, remote) {
|
|
10220
|
+
const diffs = [];
|
|
10221
|
+
const processedComponentNames = /* @__PURE__ */ new Set();
|
|
10222
|
+
for (const comp of local.components) {
|
|
10223
|
+
processedComponentNames.add(comp.name);
|
|
10224
|
+
const remoteComp = remote.components.get(comp.name);
|
|
10225
|
+
const includeGroupUuid = typeof comp.component_group_uuid === "string";
|
|
10226
|
+
const localSerialized = serializeComponent(applyDefaults(comp, COMPONENT_DEFAULTS), { includeGroupUuid });
|
|
10227
|
+
const remoteSerialized = remoteComp ? serializeComponent(applyDefaults(remoteComp, COMPONENT_DEFAULTS), { includeGroupUuid }) : null;
|
|
10228
|
+
diffs.push(diffEntity("component", comp.name, localSerialized, remoteSerialized));
|
|
10229
|
+
}
|
|
10230
|
+
for (const [name] of remote.components) {
|
|
10231
|
+
if (!processedComponentNames.has(name)) {
|
|
10232
|
+
diffs.push(diffEntity("component", name, null, "stale"));
|
|
10233
|
+
}
|
|
10234
|
+
}
|
|
10235
|
+
const processedDatasourceNames = /* @__PURE__ */ new Set();
|
|
10236
|
+
for (const ds of local.datasources) {
|
|
10237
|
+
processedDatasourceNames.add(ds.name);
|
|
10238
|
+
const remoteDs = remote.datasources.get(ds.name);
|
|
10239
|
+
const localSerialized = serializeDatasource(applyDefaults(ds, DATASOURCE_DEFAULTS));
|
|
10240
|
+
const remoteSerialized = remoteDs ? serializeDatasource(applyDefaults(remoteDs, DATASOURCE_DEFAULTS)) : null;
|
|
10241
|
+
diffs.push(diffEntity("datasource", ds.name, localSerialized, remoteSerialized));
|
|
10242
|
+
}
|
|
10243
|
+
for (const [name] of remote.datasources) {
|
|
10244
|
+
if (!processedDatasourceNames.has(name)) {
|
|
10245
|
+
diffs.push(diffEntity("datasource", name, null, "stale"));
|
|
10246
|
+
}
|
|
10247
|
+
}
|
|
10248
|
+
return {
|
|
10249
|
+
diffs,
|
|
10250
|
+
creates: diffs.filter((d) => d.action === "create").length,
|
|
10251
|
+
updates: diffs.filter((d) => d.action === "update").length,
|
|
10252
|
+
unchanged: diffs.filter((d) => d.action === "unchanged").length,
|
|
10253
|
+
stale: diffs.filter((d) => d.action === "stale").length
|
|
10254
|
+
};
|
|
10255
|
+
}
|
|
10256
|
+
|
|
10257
|
+
async function fetchRemoteSchema(spaceId) {
|
|
10258
|
+
const client = getMapiClient();
|
|
10259
|
+
const spaceIdNum = Number(spaceId);
|
|
10260
|
+
const [componentsRes, foldersRes, rawDatasources] = await Promise.all([
|
|
10261
|
+
client.components.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
|
|
10262
|
+
client.componentFolders.list({ path: { space_id: spaceIdNum }, throwOnError: true }),
|
|
10263
|
+
fetchAllPages(
|
|
10264
|
+
(page) => client.datasources.list({ path: { space_id: spaceIdNum }, query: { page }, throwOnError: true }),
|
|
10265
|
+
(data) => data?.datasources ?? []
|
|
10266
|
+
)
|
|
10267
|
+
]);
|
|
10268
|
+
const rawComponents = componentsRes.data?.components ?? [];
|
|
10269
|
+
const rawComponentFolders = foldersRes.data?.component_groups ?? [];
|
|
10270
|
+
const remote = {
|
|
10271
|
+
components: new Map(rawComponents.map((c) => [c.name, c])),
|
|
10272
|
+
componentFolders: new Map(rawComponentFolders.map((f) => [f.name, f])),
|
|
10273
|
+
datasources: new Map(rawDatasources.map((d) => [d.name, d]))
|
|
10274
|
+
};
|
|
10275
|
+
return { remote, rawComponents, rawComponentFolders, rawDatasources };
|
|
10276
|
+
}
|
|
10277
|
+
|
|
10278
|
+
function isSchemaField(value) {
|
|
10279
|
+
return isRecord(value) && "type" in value;
|
|
10280
|
+
}
|
|
10281
|
+
function toSchemaRecord(schema) {
|
|
10282
|
+
const result = {};
|
|
10283
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
10284
|
+
if (key === "_uid" || key === "component" || !isSchemaField(value)) {
|
|
10285
|
+
continue;
|
|
10286
|
+
}
|
|
10287
|
+
result[key] = value;
|
|
10288
|
+
}
|
|
10289
|
+
return result;
|
|
10290
|
+
}
|
|
10291
|
+
function buildComponentPayload(input) {
|
|
10292
|
+
if (!isRecord(input)) {
|
|
10293
|
+
return { name: "" };
|
|
10294
|
+
}
|
|
10295
|
+
return {
|
|
10296
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
10297
|
+
// Fields in COMPONENT_DEFAULTS are always sent with their reset value so that
|
|
10298
|
+
// removing a field from the local schema actually clears it on the API.
|
|
10299
|
+
// (Root-level fields are additive on MAPI update — omitting preserves the old value.)
|
|
10300
|
+
display_name: typeof input.display_name === "string" ? input.display_name : "",
|
|
10301
|
+
description: typeof input.description === "string" ? input.description : "",
|
|
10302
|
+
color: typeof input.color === "string" ? input.color : "",
|
|
10303
|
+
icon: typeof input.icon === "string" ? input.icon : "",
|
|
10304
|
+
preview_field: typeof input.preview_field === "string" ? input.preview_field : "",
|
|
10305
|
+
internal_tag_ids: Array.isArray(input.internal_tag_ids) ? input.internal_tag_ids : [],
|
|
10306
|
+
// Conditionally sent: only included when explicitly set in local schema
|
|
10307
|
+
...isRecord(input.schema) && { schema: toSchemaRecord(input.schema) },
|
|
10308
|
+
...typeof input.is_root === "boolean" && { is_root: input.is_root },
|
|
10309
|
+
...typeof input.is_nestable === "boolean" && { is_nestable: input.is_nestable },
|
|
10310
|
+
...typeof input.component_group_uuid === "string" && { component_group_uuid: input.component_group_uuid }
|
|
10311
|
+
};
|
|
10312
|
+
}
|
|
10313
|
+
function toComponentCreate(input) {
|
|
10314
|
+
return buildComponentPayload(input);
|
|
10315
|
+
}
|
|
10316
|
+
function toComponentUpdate(input) {
|
|
10317
|
+
return buildComponentPayload(input);
|
|
10318
|
+
}
|
|
10319
|
+
function toDatasourceCreate(input) {
|
|
10320
|
+
if (!isRecord(input)) {
|
|
10321
|
+
return { name: "", slug: "" };
|
|
10322
|
+
}
|
|
10323
|
+
const result = {
|
|
10324
|
+
name: typeof input.name === "string" ? input.name : "",
|
|
10325
|
+
slug: typeof input.slug === "string" ? input.slug : ""
|
|
10326
|
+
};
|
|
10327
|
+
if (Array.isArray(input.dimensions)) {
|
|
10328
|
+
result.dimensions_attributes = input.dimensions.filter((d) => isRecord(d) && typeof d.name === "string" && typeof d.entry_value === "string").map((d) => ({
|
|
10329
|
+
name: d.name,
|
|
10330
|
+
entry_value: d.entry_value
|
|
10331
|
+
}));
|
|
10332
|
+
}
|
|
10333
|
+
return result;
|
|
10334
|
+
}
|
|
10335
|
+
function toDatasourceUpdate(input, remote) {
|
|
10336
|
+
const base = toDatasourceCreate(input);
|
|
10337
|
+
const localDims = base.dimensions_attributes ?? [];
|
|
10338
|
+
const remoteDims = remote.dimensions ?? [];
|
|
10339
|
+
if (remoteDims.length === 0) {
|
|
10340
|
+
return base;
|
|
10341
|
+
}
|
|
10342
|
+
const localKeys = new Set(localDims.map((d) => `${d.name}::${d.entry_value}`));
|
|
10343
|
+
const destroyEntries = remoteDims.filter((rd) => rd.id != null && !localKeys.has(`${rd.name}::${rd.entry_value}`)).map((rd) => ({ id: rd.id, _destroy: true }));
|
|
10344
|
+
if (destroyEntries.length > 0) {
|
|
10345
|
+
return {
|
|
10346
|
+
...base,
|
|
10347
|
+
dimensions_attributes: [...localDims, ...destroyEntries]
|
|
10348
|
+
};
|
|
10349
|
+
}
|
|
10350
|
+
return base;
|
|
10351
|
+
}
|
|
10352
|
+
|
|
10353
|
+
function formatDiffOutput(result, options) {
|
|
10354
|
+
const lines = [];
|
|
10355
|
+
const byType = {
|
|
10356
|
+
component: [],
|
|
10357
|
+
datasource: []
|
|
10358
|
+
};
|
|
10359
|
+
for (const diff of result.diffs) {
|
|
10360
|
+
byType[diff.type].push(diff);
|
|
10361
|
+
}
|
|
10362
|
+
const willDelete = options?.delete ?? false;
|
|
10363
|
+
const icons = {
|
|
10364
|
+
create: chalk.green("+"),
|
|
10365
|
+
update: chalk.yellow("~"),
|
|
10366
|
+
unchanged: chalk.dim("="),
|
|
10367
|
+
stale: chalk.red("-")
|
|
10368
|
+
};
|
|
10369
|
+
const sections = [
|
|
10370
|
+
["Components", byType.component],
|
|
10371
|
+
["Datasources", byType.datasource]
|
|
10372
|
+
];
|
|
10373
|
+
for (const [label, diffs] of sections) {
|
|
10374
|
+
if (diffs.length === 0) {
|
|
10375
|
+
continue;
|
|
10376
|
+
}
|
|
10377
|
+
lines.push(chalk.bold(label));
|
|
10378
|
+
for (const diff of diffs) {
|
|
10379
|
+
const icon = icons[diff.action] ?? " ";
|
|
10380
|
+
const name = diff.action === "stale" ? chalk.red(diff.name) : diff.name;
|
|
10381
|
+
const actionLabel = diff.action === "stale" && willDelete ? "delete" : diff.action;
|
|
10382
|
+
lines.push(` ${icon} ${name} ${chalk.dim(`(${actionLabel})`)}`);
|
|
10383
|
+
if (diff.diff) {
|
|
10384
|
+
for (const line of diff.diff.split("\n")) {
|
|
10385
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
10386
|
+
lines.push(` ${chalk.green(line)}`);
|
|
10387
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
10388
|
+
lines.push(` ${chalk.red(line)}`);
|
|
10389
|
+
}
|
|
10390
|
+
}
|
|
10391
|
+
}
|
|
10392
|
+
}
|
|
10393
|
+
lines.push("");
|
|
10394
|
+
}
|
|
10395
|
+
const summary = [
|
|
10396
|
+
result.creates > 0 ? chalk.green(`${result.creates} to create`) : null,
|
|
10397
|
+
result.updates > 0 ? chalk.yellow(`${result.updates} to update`) : null,
|
|
10398
|
+
result.unchanged > 0 ? chalk.dim(`${result.unchanged} unchanged`) : null,
|
|
10399
|
+
result.stale > 0 ? chalk.red(`${result.stale} ${willDelete ? "to delete" : "stale"}`) : null
|
|
10400
|
+
].filter(Boolean).join(", ");
|
|
10401
|
+
lines.push(`Summary: ${summary}`);
|
|
10402
|
+
return lines.join("\n");
|
|
10403
|
+
}
|
|
10404
|
+
async function executePush(spaceId, local, remote, diffResult, options) {
|
|
10405
|
+
const client = getMapiClient();
|
|
10406
|
+
const spaceIdNum = Number(spaceId);
|
|
10407
|
+
let created = 0;
|
|
10408
|
+
let updated = 0;
|
|
10409
|
+
let deleted = 0;
|
|
10410
|
+
const componentDiffs = diffResult.diffs.filter((d) => d.type === "component");
|
|
10411
|
+
const componentResults = await Promise.allSettled(
|
|
10412
|
+
componentDiffs.map(async (diff) => {
|
|
10413
|
+
const localComp = local.components.find((c) => c.name === diff.name);
|
|
10414
|
+
if (diff.action === "create" && localComp) {
|
|
10415
|
+
await client.components.create({
|
|
10416
|
+
path: { space_id: spaceIdNum },
|
|
10417
|
+
body: { component: toComponentCreate(localComp) },
|
|
10418
|
+
throwOnError: true
|
|
10419
|
+
});
|
|
10420
|
+
return "created";
|
|
10421
|
+
}
|
|
10422
|
+
if (diff.action === "update" && localComp) {
|
|
10423
|
+
const existing = remote.components.get(diff.name);
|
|
10424
|
+
if (existing?.id) {
|
|
10425
|
+
await client.components.update(existing.id, {
|
|
10426
|
+
path: { space_id: spaceIdNum },
|
|
10427
|
+
body: { component: toComponentUpdate(localComp) },
|
|
10428
|
+
throwOnError: true
|
|
10429
|
+
});
|
|
10430
|
+
return "updated";
|
|
10431
|
+
}
|
|
10432
|
+
}
|
|
10433
|
+
})
|
|
10434
|
+
);
|
|
10435
|
+
for (let i = 0; i < componentResults.length; i++) {
|
|
10436
|
+
const result = componentResults[i];
|
|
10437
|
+
const diff = componentDiffs[i];
|
|
10438
|
+
if (result.status === "fulfilled") {
|
|
10439
|
+
if (result.value === "created") {
|
|
10440
|
+
created++;
|
|
10441
|
+
} else if (result.value === "updated") {
|
|
10442
|
+
updated++;
|
|
10443
|
+
}
|
|
10444
|
+
} else {
|
|
10445
|
+
const eventId = diff.action === "create" ? "push_component" : "update_component";
|
|
10446
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} component ${diff.name}`);
|
|
10447
|
+
}
|
|
10448
|
+
}
|
|
10449
|
+
const datasourceDiffs = diffResult.diffs.filter((d) => d.type === "datasource");
|
|
10450
|
+
const datasourceResults = await Promise.allSettled(
|
|
10451
|
+
datasourceDiffs.map(async (diff) => {
|
|
10452
|
+
const localDs = local.datasources.find((d) => d.name === diff.name);
|
|
10453
|
+
if (diff.action === "create" && localDs) {
|
|
10454
|
+
await client.datasources.create({
|
|
10455
|
+
path: { space_id: spaceIdNum },
|
|
10456
|
+
body: { datasource: toDatasourceCreate(localDs) },
|
|
10457
|
+
throwOnError: true
|
|
10458
|
+
});
|
|
10459
|
+
return "created";
|
|
10460
|
+
}
|
|
10461
|
+
if (diff.action === "update" && localDs) {
|
|
10462
|
+
const existing = remote.datasources.get(diff.name);
|
|
10463
|
+
if (existing?.id) {
|
|
10464
|
+
await client.datasources.update(existing.id, {
|
|
10465
|
+
path: { space_id: spaceIdNum },
|
|
10466
|
+
body: { datasource: toDatasourceUpdate(localDs, existing) },
|
|
10467
|
+
throwOnError: true
|
|
10468
|
+
});
|
|
10469
|
+
return "updated";
|
|
10470
|
+
}
|
|
10471
|
+
}
|
|
10472
|
+
})
|
|
10473
|
+
);
|
|
10474
|
+
for (let i = 0; i < datasourceResults.length; i++) {
|
|
10475
|
+
const result = datasourceResults[i];
|
|
10476
|
+
const diff = datasourceDiffs[i];
|
|
10477
|
+
if (result.status === "fulfilled") {
|
|
10478
|
+
if (result.value === "created") {
|
|
10479
|
+
created++;
|
|
10480
|
+
} else if (result.value === "updated") {
|
|
10481
|
+
updated++;
|
|
10482
|
+
}
|
|
10483
|
+
} else {
|
|
10484
|
+
const eventId = diff.action === "create" ? "push_datasource" : "update_datasource";
|
|
10485
|
+
handleAPIError(eventId, result.reason, `Failed to ${diff.action} datasource ${diff.name}`);
|
|
10486
|
+
}
|
|
10487
|
+
}
|
|
10488
|
+
if (options.delete) {
|
|
10489
|
+
const staleComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
|
|
10490
|
+
const deleteComponentResults = await Promise.allSettled(
|
|
10491
|
+
staleComponents.map(async (diff) => {
|
|
10492
|
+
const existing = remote.components.get(diff.name);
|
|
10493
|
+
if (existing?.id) {
|
|
10494
|
+
await client.components.delete(existing.id, {
|
|
10495
|
+
path: { space_id: spaceIdNum },
|
|
10496
|
+
throwOnError: true
|
|
10497
|
+
});
|
|
10498
|
+
return true;
|
|
10499
|
+
}
|
|
10500
|
+
})
|
|
10501
|
+
);
|
|
10502
|
+
for (let i = 0; i < deleteComponentResults.length; i++) {
|
|
10503
|
+
const result = deleteComponentResults[i];
|
|
10504
|
+
if (result.status === "fulfilled") {
|
|
10505
|
+
if (result.value) {
|
|
10506
|
+
deleted++;
|
|
10507
|
+
}
|
|
10508
|
+
} else {
|
|
10509
|
+
handleAPIError("delete_component", result.reason, `Failed to delete component ${staleComponents[i].name}`);
|
|
10510
|
+
}
|
|
10511
|
+
}
|
|
10512
|
+
const staleDatasources = diffResult.diffs.filter((d) => d.type === "datasource" && d.action === "stale");
|
|
10513
|
+
const deleteDatasourceResults = await Promise.allSettled(
|
|
10514
|
+
staleDatasources.map(async (diff) => {
|
|
10515
|
+
const existing = remote.datasources.get(diff.name);
|
|
10516
|
+
if (existing?.id) {
|
|
10517
|
+
await client.datasources.delete(existing.id, {
|
|
10518
|
+
path: { space_id: spaceIdNum },
|
|
10519
|
+
throwOnError: true
|
|
10520
|
+
});
|
|
10521
|
+
return true;
|
|
10522
|
+
}
|
|
10523
|
+
})
|
|
10524
|
+
);
|
|
10525
|
+
for (let i = 0; i < deleteDatasourceResults.length; i++) {
|
|
10526
|
+
const result = deleteDatasourceResults[i];
|
|
10527
|
+
if (result.status === "fulfilled") {
|
|
10528
|
+
if (result.value) {
|
|
10529
|
+
deleted++;
|
|
10530
|
+
}
|
|
10531
|
+
} else {
|
|
10532
|
+
handleAPIError("delete_datasource", result.reason, `Failed to delete datasource ${staleDatasources[i].name}`);
|
|
10533
|
+
}
|
|
10534
|
+
}
|
|
10535
|
+
}
|
|
10536
|
+
return { created, updated, deleted };
|
|
10537
|
+
}
|
|
10538
|
+
function buildChangesetEntries(diffResult, local, remote, options) {
|
|
10539
|
+
const changes = [];
|
|
10540
|
+
for (const diff of diffResult.diffs) {
|
|
10541
|
+
if (diff.action === "unchanged") {
|
|
10542
|
+
continue;
|
|
10543
|
+
}
|
|
10544
|
+
if (diff.action === "stale" && !options.delete) {
|
|
10545
|
+
continue;
|
|
10546
|
+
}
|
|
10547
|
+
const action = diff.action === "stale" ? "delete" : diff.action;
|
|
10548
|
+
let remoteSrc;
|
|
10549
|
+
let localSrc;
|
|
10550
|
+
if (diff.type === "component") {
|
|
10551
|
+
remoteSrc = remote.components.get(diff.name);
|
|
10552
|
+
localSrc = local.components.find((c) => c.name === diff.name);
|
|
10553
|
+
} else if (diff.type === "datasource") {
|
|
10554
|
+
remoteSrc = remote.datasources.get(diff.name);
|
|
10555
|
+
localSrc = local.datasources.find((d) => d.name === diff.name);
|
|
10556
|
+
}
|
|
10557
|
+
changes.push({
|
|
10558
|
+
type: diff.type,
|
|
10559
|
+
name: diff.name,
|
|
10560
|
+
action,
|
|
10561
|
+
...remoteSrc && { before: { ...remoteSrc } },
|
|
10562
|
+
...localSrc && { after: { ...localSrc } }
|
|
10563
|
+
});
|
|
10564
|
+
}
|
|
10565
|
+
return changes;
|
|
10566
|
+
}
|
|
10567
|
+
|
|
10568
|
+
async function ensureDir(dir) {
|
|
10569
|
+
await mkdir(dir, { recursive: true });
|
|
10570
|
+
}
|
|
10571
|
+
async function saveChangeset(basePath, data) {
|
|
10572
|
+
const dir = join(basePath, "schema", "changesets");
|
|
10573
|
+
await ensureDir(dir);
|
|
10574
|
+
const fileName = `${fileTimestamp(data.timestamp)}.json`;
|
|
10575
|
+
const filePath = join(dir, fileName);
|
|
10576
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
10577
|
+
return filePath;
|
|
10578
|
+
}
|
|
10579
|
+
|
|
10580
|
+
const SENTINEL_FIELDS = /* @__PURE__ */ new Set(["_uid", "component"]);
|
|
10581
|
+
function classifyFieldChanges(remoteSchema, localSchema) {
|
|
10582
|
+
const removed = [];
|
|
10583
|
+
const added = [];
|
|
10584
|
+
const typeChanged = [];
|
|
10585
|
+
const requiredAdded = [];
|
|
10586
|
+
const requiredChanged = [];
|
|
10587
|
+
for (const [field, remoteField] of Object.entries(remoteSchema)) {
|
|
10588
|
+
if (SENTINEL_FIELDS.has(field)) {
|
|
10589
|
+
continue;
|
|
10590
|
+
}
|
|
10591
|
+
if (typeof remoteField.type !== "string") {
|
|
10592
|
+
continue;
|
|
10593
|
+
}
|
|
10594
|
+
if (!(field in localSchema)) {
|
|
10595
|
+
removed.push({ field, type: remoteField.type });
|
|
10596
|
+
}
|
|
10597
|
+
}
|
|
10598
|
+
for (const [field, localField] of Object.entries(localSchema)) {
|
|
10599
|
+
if (SENTINEL_FIELDS.has(field)) {
|
|
10600
|
+
continue;
|
|
10601
|
+
}
|
|
10602
|
+
if (typeof localField.type !== "string") {
|
|
10603
|
+
continue;
|
|
10604
|
+
}
|
|
10605
|
+
if (!(field in remoteSchema)) {
|
|
10606
|
+
if (localField.required) {
|
|
10607
|
+
requiredAdded.push({ field, type: localField.type });
|
|
10608
|
+
} else {
|
|
10609
|
+
added.push({ field, type: localField.type, required: false });
|
|
10610
|
+
}
|
|
10611
|
+
} else {
|
|
10612
|
+
const remoteField = remoteSchema[field];
|
|
10613
|
+
if (typeof remoteField?.type !== "string") {
|
|
10614
|
+
continue;
|
|
10615
|
+
}
|
|
10616
|
+
if (remoteField.type !== localField.type) {
|
|
10617
|
+
typeChanged.push({ field, oldType: remoteField.type, newType: localField.type });
|
|
10618
|
+
}
|
|
10619
|
+
if (localField.required && !remoteField.required) {
|
|
10620
|
+
requiredChanged.push({ field, type: localField.type });
|
|
10621
|
+
}
|
|
10622
|
+
}
|
|
10623
|
+
}
|
|
10624
|
+
return { removed, added, typeChanged, requiredAdded, requiredChanged };
|
|
10625
|
+
}
|
|
10626
|
+
function longestCommonSubstring(a, b) {
|
|
10627
|
+
let maxLen = 0;
|
|
10628
|
+
for (let i = 0; i < a.length; i++) {
|
|
10629
|
+
for (let j = 0; j < b.length; j++) {
|
|
10630
|
+
let len = 0;
|
|
10631
|
+
while (i + len < a.length && j + len < b.length && a[i + len] === b[j + len]) {
|
|
10632
|
+
len++;
|
|
10633
|
+
}
|
|
10634
|
+
if (len > maxLen) {
|
|
10635
|
+
maxLen = len;
|
|
10636
|
+
}
|
|
10637
|
+
}
|
|
10638
|
+
}
|
|
10639
|
+
return maxLen;
|
|
10640
|
+
}
|
|
10641
|
+
function nameSimilarity(a, b) {
|
|
10642
|
+
const longer = Math.max(a.length, b.length);
|
|
10643
|
+
if (longer === 0) {
|
|
10644
|
+
return 1;
|
|
10645
|
+
}
|
|
10646
|
+
return longestCommonSubstring(a, b) / longer;
|
|
10647
|
+
}
|
|
10648
|
+
function detectRenames(removed, added) {
|
|
10649
|
+
const renames = [];
|
|
10650
|
+
const usedRemoved = /* @__PURE__ */ new Set();
|
|
10651
|
+
const usedAdded = /* @__PURE__ */ new Set();
|
|
10652
|
+
const addedByType = /* @__PURE__ */ new Map();
|
|
10653
|
+
for (const addedField of added) {
|
|
10654
|
+
if (!addedByType.has(addedField.type)) {
|
|
10655
|
+
addedByType.set(addedField.type, []);
|
|
10656
|
+
}
|
|
10657
|
+
addedByType.get(addedField.type).push(addedField);
|
|
10658
|
+
}
|
|
10659
|
+
const isSinglePair = removed.length === 1 && added.length === 1;
|
|
10660
|
+
for (const removedField of removed) {
|
|
10661
|
+
const candidates = addedByType.get(removedField.type) ?? [];
|
|
10662
|
+
const availableCandidates = candidates.filter((c) => !usedAdded.has(c.field));
|
|
10663
|
+
if (availableCandidates.length === 0) {
|
|
10664
|
+
continue;
|
|
10665
|
+
}
|
|
10666
|
+
let bestCandidate = availableCandidates[0];
|
|
10667
|
+
let bestScore = nameSimilarity(removedField.field, bestCandidate.field);
|
|
10668
|
+
for (let i = 1; i < availableCandidates.length; i++) {
|
|
10669
|
+
const score = nameSimilarity(removedField.field, availableCandidates[i].field);
|
|
10670
|
+
if (score > bestScore) {
|
|
10671
|
+
bestScore = score;
|
|
10672
|
+
bestCandidate = availableCandidates[i];
|
|
10673
|
+
}
|
|
10674
|
+
}
|
|
10675
|
+
if (!isSinglePair && bestScore < 0.3) {
|
|
10676
|
+
continue;
|
|
10677
|
+
}
|
|
10678
|
+
renames.push({ oldField: removedField.field, newField: bestCandidate.field, fieldType: removedField.type });
|
|
10679
|
+
usedRemoved.add(removedField.field);
|
|
10680
|
+
usedAdded.add(bestCandidate.field);
|
|
10681
|
+
}
|
|
10682
|
+
const unmatchedRemoved = removed.filter((r) => !usedRemoved.has(r.field));
|
|
10683
|
+
const unmatchedAdded = added.filter((a) => !usedAdded.has(a.field));
|
|
10684
|
+
return { renames, unmatchedRemoved, unmatchedAdded };
|
|
10685
|
+
}
|
|
10686
|
+
function analyzeBreakingChanges(diffResult, local, remote) {
|
|
10687
|
+
const results = [];
|
|
10688
|
+
const updatedComponents = diffResult.diffs.filter(
|
|
10689
|
+
(d) => d.type === "component" && d.action === "update"
|
|
10690
|
+
);
|
|
10691
|
+
for (const diff of updatedComponents) {
|
|
10692
|
+
const localComp = local.components.find((c) => c.name === diff.name);
|
|
10693
|
+
const remoteComp = remote.components.get(diff.name);
|
|
10694
|
+
if (!localComp?.schema || !remoteComp?.schema) {
|
|
10695
|
+
continue;
|
|
10696
|
+
}
|
|
10697
|
+
const classification = classifyFieldChanges(
|
|
10698
|
+
remoteComp.schema,
|
|
10699
|
+
localComp.schema
|
|
10700
|
+
);
|
|
10701
|
+
const changes = [];
|
|
10702
|
+
const { renames, unmatchedRemoved } = detectRenames(classification.removed, classification.added);
|
|
10703
|
+
for (const rename of renames) {
|
|
10704
|
+
changes.push({ kind: "rename", field: rename.newField, oldField: rename.oldField });
|
|
10705
|
+
}
|
|
10706
|
+
for (const removed of unmatchedRemoved) {
|
|
10707
|
+
changes.push({ kind: "removed", field: removed.field });
|
|
10708
|
+
}
|
|
10709
|
+
for (const tc of classification.typeChanged) {
|
|
10710
|
+
changes.push({ kind: "type_changed", field: tc.field, oldType: tc.oldType, newType: tc.newType });
|
|
10711
|
+
}
|
|
10712
|
+
for (const ra of classification.requiredAdded) {
|
|
10713
|
+
changes.push({ kind: "required_added", field: ra.field, fieldType: ra.type });
|
|
10714
|
+
}
|
|
10715
|
+
for (const rc of classification.requiredChanged) {
|
|
10716
|
+
changes.push({ kind: "required_changed", field: rc.field, fieldType: rc.type });
|
|
10717
|
+
}
|
|
10718
|
+
if (changes.length > 0) {
|
|
10719
|
+
results.push({ componentName: diff.name, changes });
|
|
10720
|
+
}
|
|
10721
|
+
}
|
|
10722
|
+
return results;
|
|
10723
|
+
}
|
|
10724
|
+
|
|
10725
|
+
const COMPATIBLE_TYPES = /* @__PURE__ */ new Set(["text:textarea", "textarea:text"]);
|
|
10726
|
+
function defaultForType(fieldType) {
|
|
10727
|
+
switch (fieldType) {
|
|
10728
|
+
case "text":
|
|
10729
|
+
case "textarea":
|
|
10730
|
+
case "markdown":
|
|
10731
|
+
return `''`;
|
|
10732
|
+
case "number":
|
|
10733
|
+
return "0";
|
|
10734
|
+
case "boolean":
|
|
10735
|
+
return "false";
|
|
10736
|
+
default:
|
|
10737
|
+
return null;
|
|
10738
|
+
}
|
|
10739
|
+
}
|
|
10740
|
+
function typeConversion(field, oldType, newType) {
|
|
10741
|
+
const key = `${oldType}:${newType}`;
|
|
10742
|
+
if (COMPATIBLE_TYPES.has(key)) {
|
|
10743
|
+
return null;
|
|
10744
|
+
}
|
|
10745
|
+
const accessor = `block.${field}`;
|
|
10746
|
+
switch (key) {
|
|
10747
|
+
case "text:number":
|
|
10748
|
+
return `${accessor} = Number(${accessor}) || 0;`;
|
|
10749
|
+
case "number:text":
|
|
10750
|
+
return `${accessor} = String(${accessor});`;
|
|
10751
|
+
case "text:boolean":
|
|
10752
|
+
return `${accessor} = !!${accessor};`;
|
|
10753
|
+
case "boolean:text":
|
|
10754
|
+
return `${accessor} = String(${accessor});`;
|
|
10755
|
+
default:
|
|
10756
|
+
return `${accessor}; // TODO: convert from ${oldType} to ${newType}`;
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10759
|
+
function renderMigrationCode(changes) {
|
|
10760
|
+
const lines = [];
|
|
10761
|
+
lines.push(" // Review this migration before running it against your space.");
|
|
10762
|
+
lines.push(" // Generated migrations are scaffolds and may need manual adjustments.");
|
|
10763
|
+
lines.push(" // Example rename migration:");
|
|
10764
|
+
lines.push(" // block.new_field = block.old_field;");
|
|
10765
|
+
lines.push(" // delete block.old_field;");
|
|
10766
|
+
lines.push("");
|
|
10767
|
+
for (const change of changes) {
|
|
10768
|
+
switch (change.kind) {
|
|
10769
|
+
case "rename":
|
|
10770
|
+
lines.push(` // Rename: ${change.oldField} \u2192 ${change.field}`);
|
|
10771
|
+
lines.push(` if ('${change.oldField}' in block) {`);
|
|
10772
|
+
lines.push(` block.${change.field} = block.${change.oldField};`);
|
|
10773
|
+
lines.push(` delete block.${change.oldField};`);
|
|
10774
|
+
lines.push(` }`);
|
|
10775
|
+
break;
|
|
10776
|
+
case "removed":
|
|
10777
|
+
if (change.renameHint) {
|
|
10778
|
+
lines.push(` // If '${change.field}' was renamed to '${change.renameHint.newField}', uncomment:`);
|
|
10779
|
+
lines.push(` // block.${change.renameHint.newField} = block.${change.field};`);
|
|
10780
|
+
} else {
|
|
10781
|
+
lines.push(` // Removed field: ${change.field}`);
|
|
10782
|
+
}
|
|
10783
|
+
lines.push(` delete block.${change.field};`);
|
|
10784
|
+
break;
|
|
10785
|
+
case "type_changed": {
|
|
10786
|
+
const conversion = typeConversion(change.field, change.oldType, change.newType);
|
|
10787
|
+
if (conversion) {
|
|
10788
|
+
lines.push(` // Type change: ${change.field} (${change.oldType} \u2192 ${change.newType})`);
|
|
10789
|
+
lines.push(` ${conversion}`);
|
|
10790
|
+
}
|
|
10791
|
+
break;
|
|
10792
|
+
}
|
|
10793
|
+
case "required_added": {
|
|
10794
|
+
const defaultValue = defaultForType(change.fieldType);
|
|
10795
|
+
lines.push(` // New required field: ${change.field} (${change.fieldType})`);
|
|
10796
|
+
if (defaultValue !== null) {
|
|
10797
|
+
lines.push(` // TODO: provide a meaningful default value`);
|
|
10798
|
+
lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
|
|
10799
|
+
} else {
|
|
10800
|
+
lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
|
|
10801
|
+
lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
|
|
10802
|
+
}
|
|
10803
|
+
break;
|
|
10804
|
+
}
|
|
10805
|
+
case "required_changed": {
|
|
10806
|
+
const defaultValue = defaultForType(change.fieldType);
|
|
10807
|
+
lines.push(` // Field is now required: ${change.field} (${change.fieldType})`);
|
|
10808
|
+
lines.push(` // Existing stories may have null/undefined values \u2014 provide a default for those.`);
|
|
10809
|
+
if (defaultValue !== null) {
|
|
10810
|
+
lines.push(` // TODO: provide a meaningful default value`);
|
|
10811
|
+
lines.push(` block.${change.field} = block.${change.field} ?? ${defaultValue};`);
|
|
10812
|
+
} else {
|
|
10813
|
+
lines.push(` // TODO: provide a default value appropriate for the '${change.fieldType}' type`);
|
|
10814
|
+
lines.push(` // block.${change.field} = block.${change.field} ?? <default>;`);
|
|
10815
|
+
}
|
|
10816
|
+
break;
|
|
10817
|
+
}
|
|
10818
|
+
}
|
|
10819
|
+
lines.push("");
|
|
10820
|
+
}
|
|
10821
|
+
const body = lines.length > 0 ? `
|
|
10822
|
+
${lines.join("\n")}` : "\n";
|
|
10823
|
+
return `export default function (block) {${body} return block;
|
|
10824
|
+
}
|
|
10825
|
+
`;
|
|
10826
|
+
}
|
|
10827
|
+
async function writeMigrationFile(options) {
|
|
10828
|
+
const { spaceId, componentName, code, timestamp, basePath } = options;
|
|
10829
|
+
const dir = resolvePath(basePath, `migrations/${spaceId}`);
|
|
10830
|
+
await mkdir(dir, { recursive: true });
|
|
10831
|
+
const fileName = `${componentName}.${fileTimestamp(timestamp)}.js`;
|
|
10832
|
+
const filePath = join(dir, fileName);
|
|
10833
|
+
await writeFile(filePath, code, "utf-8");
|
|
10834
|
+
return filePath;
|
|
10835
|
+
}
|
|
10836
|
+
|
|
10837
|
+
const DEFAULT_GROUPS_FILENAME = "groups.json";
|
|
10838
|
+
const CONSOLIDATED_COMPONENTS_FILENAME = "components.json";
|
|
10839
|
+
async function writeLocalComponents({
|
|
10840
|
+
space,
|
|
10841
|
+
basePath,
|
|
10842
|
+
resolved,
|
|
10843
|
+
diffResult,
|
|
10844
|
+
deleteRemoved,
|
|
10845
|
+
ui,
|
|
10846
|
+
logger
|
|
10847
|
+
}) {
|
|
10848
|
+
const componentsDir = resolveCommandPath(directories.components, space, basePath);
|
|
10849
|
+
const consolidatedPath = join(componentsDir, CONSOLIDATED_COMPONENTS_FILENAME);
|
|
10850
|
+
if (await fileExists(consolidatedPath)) {
|
|
10851
|
+
ui.warn(
|
|
10852
|
+
`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.`
|
|
10853
|
+
);
|
|
10854
|
+
}
|
|
10855
|
+
for (const component of resolved.components) {
|
|
10856
|
+
const filePath = join(componentsDir, `${sanitizeFilename(component.name || "")}.json`);
|
|
10857
|
+
await saveToFile(filePath, JSON.stringify(component, null, 2));
|
|
10858
|
+
}
|
|
10859
|
+
const groupsPath = join(componentsDir, DEFAULT_GROUPS_FILENAME);
|
|
10860
|
+
if (await fileExists(groupsPath)) {
|
|
10861
|
+
try {
|
|
10862
|
+
await unlink(groupsPath);
|
|
10863
|
+
logger.info("Removed stale local groups file", { path: displayPath(groupsPath, basePath) });
|
|
10864
|
+
} catch (error) {
|
|
10865
|
+
if (error.code !== "ENOENT") {
|
|
10866
|
+
throw error;
|
|
10867
|
+
}
|
|
10868
|
+
}
|
|
10869
|
+
}
|
|
10870
|
+
if (deleteRemoved) {
|
|
10871
|
+
const staleComponents = diffResult.diffs.filter(
|
|
10872
|
+
(d) => d.type === "component" && d.action === "stale"
|
|
10873
|
+
);
|
|
10874
|
+
for (const stale of staleComponents) {
|
|
10875
|
+
const filePath = join(componentsDir, `${sanitizeFilename(stale.name)}.json`);
|
|
10876
|
+
try {
|
|
10877
|
+
await unlink(filePath);
|
|
10878
|
+
logger.info("Removed stale local component file", { path: displayPath(filePath, basePath) });
|
|
10879
|
+
} catch (error) {
|
|
10880
|
+
if (error.code !== "ENOENT") {
|
|
10881
|
+
throw error;
|
|
10882
|
+
}
|
|
10883
|
+
}
|
|
10884
|
+
}
|
|
10885
|
+
}
|
|
10886
|
+
logger.info("Wrote local component files", {
|
|
10887
|
+
space,
|
|
10888
|
+
componentsWritten: resolved.components.length
|
|
10889
|
+
});
|
|
10890
|
+
}
|
|
10891
|
+
|
|
10892
|
+
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) => {
|
|
10893
|
+
const ui = getUI();
|
|
10894
|
+
const logger = getLogger();
|
|
10895
|
+
const reporter = getReporter();
|
|
10896
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
10897
|
+
const { state } = session();
|
|
10898
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Pushing schema...");
|
|
10899
|
+
logger.info("Schema push started", { entryFile, space });
|
|
10900
|
+
if (!requireAuthentication(state, verbose)) {
|
|
10901
|
+
return;
|
|
10902
|
+
}
|
|
10903
|
+
if (!space) {
|
|
10904
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
10905
|
+
return;
|
|
10906
|
+
}
|
|
10907
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
10908
|
+
try {
|
|
10909
|
+
const loadSpinner = ui.createSpinner("Resolving schema...");
|
|
10910
|
+
let local;
|
|
10911
|
+
try {
|
|
10912
|
+
local = await loadSchema(entryFile);
|
|
10913
|
+
} catch (maybeError) {
|
|
10914
|
+
loadSpinner.failed("Failed to resolve schema");
|
|
10915
|
+
handleError(toError(maybeError), verbose);
|
|
10916
|
+
return;
|
|
10917
|
+
}
|
|
10918
|
+
loadSpinner.succeed(`Found: ${local.components.length} components, ${local.datasources.length} datasources`);
|
|
10919
|
+
const totalLocal = local.components.length + local.datasources.length;
|
|
10920
|
+
if (totalLocal === 0) {
|
|
10921
|
+
ui.warn("No components or datasources found in the entry file. Verify the file exports schema definitions.");
|
|
10922
|
+
return;
|
|
10923
|
+
}
|
|
10924
|
+
const remoteSpinner = ui.createSpinner(`Fetching remote state from space ${space}...`);
|
|
10925
|
+
let remoteResult;
|
|
10926
|
+
try {
|
|
10927
|
+
remoteResult = await fetchRemoteSchema(space);
|
|
10928
|
+
} catch (maybeError) {
|
|
10929
|
+
remoteSpinner.failed("Failed to fetch remote schema");
|
|
10930
|
+
handleError(toError(maybeError), verbose);
|
|
10931
|
+
return;
|
|
10932
|
+
}
|
|
10933
|
+
const { remote, rawComponents, rawComponentFolders, rawDatasources } = remoteResult;
|
|
10934
|
+
remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.datasources.size} datasources`);
|
|
10935
|
+
const diffResult = diffSchema(local, remote);
|
|
10936
|
+
ui.br();
|
|
10937
|
+
ui.log(formatDiffOutput(diffResult, { delete: options.delete }));
|
|
10938
|
+
if (options.migrations) {
|
|
10939
|
+
const breakingChanges = analyzeBreakingChanges(diffResult, local, remote);
|
|
10940
|
+
if (breakingChanges.length > 0) {
|
|
10941
|
+
const totalChanges = breakingChanges.reduce((sum, c) => sum + c.changes.length, 0);
|
|
10942
|
+
ui.br();
|
|
10943
|
+
ui.warn(`${totalChanges} breaking change(s) detected in ${breakingChanges.length} component(s).`);
|
|
10944
|
+
ui.info("Generated migrations are scaffolds. Review and adjust them before running `storyblok migrations run`.");
|
|
10945
|
+
if (!options.dryRun) {
|
|
10946
|
+
const explicitMigrations = command.getOptionValueSource("migrations") === "cli";
|
|
10947
|
+
const shouldGenerate = explicitMigrations || await confirm({
|
|
10948
|
+
message: "Generate migration files for breaking changes?",
|
|
10949
|
+
default: true
|
|
10950
|
+
});
|
|
10951
|
+
if (shouldGenerate) {
|
|
10952
|
+
const migrationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10953
|
+
const resolvedBase = resolvePath(basePath, "");
|
|
10954
|
+
for (const comp of breakingChanges) {
|
|
10955
|
+
const renames = comp.changes.filter((c) => c.kind === "rename");
|
|
10956
|
+
if (renames.length > 0 && explicitMigrations) {
|
|
10957
|
+
for (const r of renames) {
|
|
10958
|
+
if (r.kind === "rename") {
|
|
10959
|
+
ui.log(` Assumed rename in '${comp.componentName}': ${r.oldField} \u2192 ${r.field}`);
|
|
10960
|
+
}
|
|
10961
|
+
}
|
|
10962
|
+
}
|
|
10963
|
+
if (renames.length > 0 && !explicitMigrations) {
|
|
10964
|
+
ui.br();
|
|
10965
|
+
ui.log(`Detected renames in '${comp.componentName}':`);
|
|
10966
|
+
for (const r of renames) {
|
|
10967
|
+
if (r.kind === "rename") {
|
|
10968
|
+
ui.log(` ${r.oldField} \u2192 ${r.field}`);
|
|
10969
|
+
}
|
|
10970
|
+
}
|
|
10971
|
+
const renameConfirmed = await confirm({
|
|
10972
|
+
message: "Are these renames correct?",
|
|
10973
|
+
default: true
|
|
10974
|
+
});
|
|
10975
|
+
if (!renameConfirmed) {
|
|
10976
|
+
comp.changes = comp.changes.map((c) => {
|
|
10977
|
+
if (c.kind === "rename") {
|
|
10978
|
+
return { kind: "removed", field: c.oldField, renameHint: { newField: c.field } };
|
|
10979
|
+
}
|
|
10980
|
+
return c;
|
|
10981
|
+
});
|
|
10982
|
+
}
|
|
10983
|
+
}
|
|
10984
|
+
const code = renderMigrationCode(comp.changes);
|
|
10985
|
+
const path = await writeMigrationFile({
|
|
10986
|
+
spaceId: space,
|
|
10987
|
+
componentName: comp.componentName,
|
|
10988
|
+
code,
|
|
10989
|
+
timestamp: migrationTimestamp,
|
|
10990
|
+
basePath: resolvedBase
|
|
10991
|
+
});
|
|
10992
|
+
const migrationPath = displayPath(path, basePath);
|
|
10993
|
+
logger.info("Migration generated", { component: comp.componentName, path: migrationPath });
|
|
10994
|
+
ui.log(` Generated: ${migrationPath}`);
|
|
10995
|
+
}
|
|
10996
|
+
ui.br();
|
|
10997
|
+
ui.info(`Run migrations when ready: storyblok migrations run --space ${space}`);
|
|
10998
|
+
}
|
|
10999
|
+
}
|
|
11000
|
+
}
|
|
11001
|
+
}
|
|
11002
|
+
if (options.delete) {
|
|
11003
|
+
const deletedComponents = diffResult.diffs.filter((d) => d.type === "component" && d.action === "stale");
|
|
11004
|
+
for (const comp of deletedComponents) {
|
|
11005
|
+
ui.warn(`Component '${comp.name}' will be deleted. Stories using it will have out-of-schema content.`);
|
|
11006
|
+
}
|
|
11007
|
+
}
|
|
11008
|
+
if (diffResult.stale > 0 && !options.delete) {
|
|
11009
|
+
ui.warn(`${diffResult.stale} stale entity(s) exist remotely but not in schema. Use --delete to remove.`);
|
|
11010
|
+
}
|
|
11011
|
+
if (options.dryRun) {
|
|
11012
|
+
ui.info("Dry run \u2014 no changes applied.");
|
|
11013
|
+
logger.info("Dry run completed", { creates: diffResult.creates, updates: diffResult.updates });
|
|
11014
|
+
return;
|
|
11015
|
+
}
|
|
11016
|
+
const resolvedPath = resolvePath(basePath, "");
|
|
11017
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11018
|
+
const changesetPath = await saveChangeset(resolvedPath, {
|
|
11019
|
+
timestamp,
|
|
11020
|
+
spaceId: Number(space),
|
|
11021
|
+
remote: { components: rawComponents, componentFolders: rawComponentFolders, datasources: rawDatasources },
|
|
11022
|
+
changes: buildChangesetEntries(diffResult, local, remote, { delete: options.delete })
|
|
11023
|
+
});
|
|
11024
|
+
logger.info("Changeset saved", { path: displayPath(changesetPath, basePath) });
|
|
11025
|
+
const nothingToPush = diffResult.creates === 0 && diffResult.updates === 0 && (!options.delete || diffResult.stale === 0);
|
|
11026
|
+
if (nothingToPush) {
|
|
11027
|
+
ui.ok("Everything up to date \u2014 nothing to push.");
|
|
11028
|
+
} else {
|
|
11029
|
+
const pushSpinner = ui.createSpinner("Pushing schema...");
|
|
11030
|
+
let result;
|
|
11031
|
+
try {
|
|
11032
|
+
result = await executePush(space, local, remote, diffResult, { delete: options.delete });
|
|
11033
|
+
} catch (error) {
|
|
11034
|
+
pushSpinner.failed("Failed to push schema");
|
|
11035
|
+
throw error;
|
|
11036
|
+
}
|
|
11037
|
+
summary.total = result.created + result.updated + result.deleted;
|
|
11038
|
+
summary.succeeded = summary.total;
|
|
11039
|
+
pushSpinner.succeed(`Pushed ${result.created} creations, ${result.updated} updates${result.deleted > 0 ? `, ${result.deleted} deletions` : ""}.`);
|
|
11040
|
+
}
|
|
11041
|
+
if (options.writeComponents) {
|
|
11042
|
+
try {
|
|
11043
|
+
await writeLocalComponents({
|
|
11044
|
+
space,
|
|
11045
|
+
basePath,
|
|
11046
|
+
resolved: local,
|
|
11047
|
+
diffResult,
|
|
11048
|
+
deleteRemoved: options.delete,
|
|
11049
|
+
ui,
|
|
11050
|
+
logger
|
|
11051
|
+
});
|
|
11052
|
+
} catch (writeError) {
|
|
11053
|
+
ui.warn(`Failed to write local component files: ${toError(writeError).message}`);
|
|
11054
|
+
logger.warn("Failed to write local component files", { error: toError(writeError).message });
|
|
11055
|
+
}
|
|
11056
|
+
}
|
|
11057
|
+
} catch (maybeError) {
|
|
11058
|
+
summary.failed += 1;
|
|
11059
|
+
handleError(toError(maybeError), verbose);
|
|
11060
|
+
} finally {
|
|
11061
|
+
logger.info("Schema push finished", { summary });
|
|
11062
|
+
reporter.addSummary("schemaPushResults", summary);
|
|
11063
|
+
reporter.finalize();
|
|
11064
|
+
}
|
|
11065
|
+
});
|
|
11066
|
+
|
|
11067
|
+
function buildGroupPathByUuid(folders) {
|
|
11068
|
+
const byUuid = new Map(folders.map((folder) => [folder.uuid, folder]));
|
|
11069
|
+
const cache = /* @__PURE__ */ new Map();
|
|
11070
|
+
function pathFor(uuid, visited) {
|
|
11071
|
+
if (!uuid) {
|
|
11072
|
+
return [];
|
|
11073
|
+
}
|
|
11074
|
+
const cached = cache.get(uuid);
|
|
11075
|
+
if (cached) {
|
|
11076
|
+
return cached;
|
|
11077
|
+
}
|
|
11078
|
+
const folder = byUuid.get(uuid);
|
|
11079
|
+
if (!folder) {
|
|
11080
|
+
return [];
|
|
11081
|
+
}
|
|
11082
|
+
if (visited.has(uuid)) {
|
|
11083
|
+
return [];
|
|
11084
|
+
}
|
|
11085
|
+
visited.add(uuid);
|
|
11086
|
+
const path = [...pathFor(folder.parent_uuid, visited), slugify(folder.name)];
|
|
11087
|
+
cache.set(uuid, path);
|
|
11088
|
+
return path;
|
|
11089
|
+
}
|
|
11090
|
+
for (const folder of folders) {
|
|
11091
|
+
pathFor(folder.uuid, /* @__PURE__ */ new Set());
|
|
11092
|
+
}
|
|
11093
|
+
return cache;
|
|
11094
|
+
}
|
|
11095
|
+
|
|
11096
|
+
const FIELD_STRIP_KEYS = /* @__PURE__ */ new Set(["id", "pos"]);
|
|
11097
|
+
function toCamelCase(str) {
|
|
11098
|
+
return str.toLowerCase().replace(/[\s_-]+(.)/g, (_, char) => char.toUpperCase());
|
|
11099
|
+
}
|
|
11100
|
+
function toKebabCase(str) {
|
|
11101
|
+
return str.replace(/[\s_]+/g, "-").replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
11102
|
+
}
|
|
11103
|
+
function componentVarName(name) {
|
|
11104
|
+
return `${toCamelCase(name)}Block`;
|
|
11105
|
+
}
|
|
11106
|
+
function datasourceVarName(name) {
|
|
11107
|
+
return `${toCamelCase(name)}Datasource`;
|
|
11108
|
+
}
|
|
11109
|
+
function componentFileName(name) {
|
|
11110
|
+
return toKebabCase(name);
|
|
11111
|
+
}
|
|
11112
|
+
function datasourceFileName(datasource) {
|
|
11113
|
+
return toKebabCase(datasource.slug || datasource.name);
|
|
11114
|
+
}
|
|
11115
|
+
function toDslField(field) {
|
|
11116
|
+
const { component_whitelist, datasource_slug, ...rest } = field;
|
|
11117
|
+
const out = { ...rest };
|
|
11118
|
+
if (component_whitelist !== void 0) {
|
|
11119
|
+
out.allow = component_whitelist;
|
|
11120
|
+
}
|
|
11121
|
+
if (datasource_slug !== void 0) {
|
|
11122
|
+
out.datasource = datasource_slug;
|
|
11123
|
+
}
|
|
11124
|
+
return out;
|
|
11125
|
+
}
|
|
11126
|
+
function generateFieldCode(fieldName, fieldData, depth) {
|
|
11127
|
+
const clean = toDslField(stripKeys(fieldData, FIELD_STRIP_KEYS));
|
|
11128
|
+
return `defineField('${fieldName.replace(/'/g, "\\'")}', ${formatValue(clean, depth)})`;
|
|
11129
|
+
}
|
|
11130
|
+
function sortSchemaByPos(schema) {
|
|
11131
|
+
return Object.entries(schema).filter(([key]) => key !== "_uid" && key !== "component").sort(([, a], [, b]) => {
|
|
11132
|
+
const posA = typeof a.pos === "number" ? a.pos : Infinity;
|
|
11133
|
+
const posB = typeof b.pos === "number" ? b.pos : Infinity;
|
|
11134
|
+
return posA - posB;
|
|
11135
|
+
});
|
|
11136
|
+
}
|
|
11137
|
+
function generateComponentFile(component) {
|
|
11138
|
+
const lines = [];
|
|
11139
|
+
lines.push("import {");
|
|
11140
|
+
lines.push(" defineBlock,");
|
|
11141
|
+
lines.push(" defineField,");
|
|
11142
|
+
lines.push("} from '@storyblok/schema';");
|
|
11143
|
+
lines.push("");
|
|
11144
|
+
const varName = componentVarName(component.name);
|
|
11145
|
+
lines.push(`export const ${varName} = defineBlock({`);
|
|
11146
|
+
const clean = stripKeys(component, COMPONENT_STRIP_KEYS);
|
|
11147
|
+
delete clean.component_group_uuid;
|
|
11148
|
+
const orderedKeys = [];
|
|
11149
|
+
if (clean.name !== void 0) {
|
|
11150
|
+
orderedKeys.push("name");
|
|
11151
|
+
}
|
|
11152
|
+
if (clean.display_name !== void 0) {
|
|
11153
|
+
orderedKeys.push("display_name");
|
|
11154
|
+
}
|
|
11155
|
+
if (clean.is_root !== void 0) {
|
|
11156
|
+
orderedKeys.push("is_root");
|
|
11157
|
+
}
|
|
11158
|
+
if (clean.is_nestable !== void 0) {
|
|
11159
|
+
orderedKeys.push("is_nestable");
|
|
11160
|
+
}
|
|
11161
|
+
const handled = /* @__PURE__ */ new Set(["name", "display_name", "is_root", "is_nestable", "schema"]);
|
|
11162
|
+
for (const key of Object.keys(clean).sort()) {
|
|
11163
|
+
if (!handled.has(key)) {
|
|
11164
|
+
orderedKeys.push(key);
|
|
11165
|
+
}
|
|
11166
|
+
}
|
|
11167
|
+
for (const key of orderedKeys) {
|
|
11168
|
+
lines.push(`${INDENT}${key}: ${formatValue(clean[key], 1)},`);
|
|
11169
|
+
}
|
|
11170
|
+
if (clean.schema && typeof clean.schema === "object") {
|
|
11171
|
+
const schema = clean.schema;
|
|
11172
|
+
const sortedFields = sortSchemaByPos(schema);
|
|
11173
|
+
if (sortedFields.length > 0) {
|
|
11174
|
+
lines.push(`${INDENT}fields: [`);
|
|
11175
|
+
for (const [fieldName, fieldData] of sortedFields) {
|
|
11176
|
+
const fieldCode = generateFieldCode(fieldName, fieldData, 2);
|
|
11177
|
+
lines.push(`${INDENT}${INDENT}${fieldCode},`);
|
|
11178
|
+
}
|
|
11179
|
+
lines.push(`${INDENT}],`);
|
|
11180
|
+
} else {
|
|
11181
|
+
lines.push(`${INDENT}fields: [],`);
|
|
11182
|
+
}
|
|
11183
|
+
}
|
|
11184
|
+
lines.push("});");
|
|
11185
|
+
lines.push("");
|
|
11186
|
+
return lines.join("\n");
|
|
11187
|
+
}
|
|
11188
|
+
function generateDatasourceFile(datasource) {
|
|
11189
|
+
const lines = [];
|
|
11190
|
+
lines.push("import { defineDatasource } from '@storyblok/schema';");
|
|
11191
|
+
lines.push("");
|
|
11192
|
+
const varName = datasourceVarName(datasource.name);
|
|
11193
|
+
lines.push(`export const ${varName} = defineDatasource({`);
|
|
11194
|
+
const clean = stripKeys(datasource, DATASOURCE_STRIP_KEYS);
|
|
11195
|
+
if (clean.name !== void 0) {
|
|
11196
|
+
lines.push(`${INDENT}name: ${formatValue(clean.name, 1)},`);
|
|
11197
|
+
}
|
|
11198
|
+
if (clean.slug !== void 0) {
|
|
11199
|
+
lines.push(`${INDENT}slug: ${formatValue(clean.slug, 1)},`);
|
|
11200
|
+
}
|
|
11201
|
+
const handled = /* @__PURE__ */ new Set(["name", "slug"]);
|
|
11202
|
+
for (const [key, value] of Object.entries(clean).sort(([a], [b]) => a.localeCompare(b))) {
|
|
11203
|
+
if (!handled.has(key)) {
|
|
11204
|
+
lines.push(`${INDENT}${key}: ${formatValue(value, 1)},`);
|
|
11205
|
+
}
|
|
11206
|
+
}
|
|
11207
|
+
lines.push("});");
|
|
11208
|
+
lines.push("");
|
|
11209
|
+
return lines.join("\n");
|
|
11210
|
+
}
|
|
11211
|
+
function generateSchemaFile(components, datasources, groupPathByComponentName = /* @__PURE__ */ new Map()) {
|
|
11212
|
+
const lines = [];
|
|
11213
|
+
lines.push("import type { Schema as InferSchema, Story as InferStory } from '@storyblok/schema';");
|
|
11214
|
+
lines.push("import type { MapiStory as InferStoryMapi } from '@storyblok/schema';");
|
|
11215
|
+
lines.push("");
|
|
11216
|
+
for (const component of components) {
|
|
11217
|
+
const varName = componentVarName(component.name);
|
|
11218
|
+
const fileName = componentFileName(component.name);
|
|
11219
|
+
const segments = groupPathByComponentName.get(component.name) ?? [];
|
|
11220
|
+
const subPath = segments.length > 0 ? `${segments.join("/")}/` : "";
|
|
11221
|
+
lines.push(`import { ${varName} } from './blocks/${subPath}${fileName}';`);
|
|
11222
|
+
}
|
|
11223
|
+
for (const datasource of datasources) {
|
|
11224
|
+
const varName = datasourceVarName(datasource.name);
|
|
11225
|
+
const fileName = datasourceFileName(datasource);
|
|
11226
|
+
lines.push(`import { ${varName} } from './datasources/${fileName}';`);
|
|
11227
|
+
}
|
|
11228
|
+
lines.push("");
|
|
11229
|
+
lines.push("export const schema = {");
|
|
11230
|
+
if (components.length > 0) {
|
|
11231
|
+
lines.push(" blocks: {");
|
|
11232
|
+
for (const component of components) {
|
|
11233
|
+
const varName = componentVarName(component.name);
|
|
11234
|
+
lines.push(` ${varName},`);
|
|
11235
|
+
}
|
|
11236
|
+
lines.push(" },");
|
|
11237
|
+
}
|
|
11238
|
+
if (datasources.length > 0) {
|
|
11239
|
+
lines.push(" datasources: {");
|
|
11240
|
+
for (const datasource of datasources) {
|
|
11241
|
+
const varName = datasourceVarName(datasource.name);
|
|
11242
|
+
lines.push(` ${varName},`);
|
|
11243
|
+
}
|
|
11244
|
+
lines.push(" },");
|
|
11245
|
+
}
|
|
11246
|
+
lines.push("};");
|
|
11247
|
+
lines.push("");
|
|
11248
|
+
lines.push("export type Schema = InferSchema<typeof schema>;");
|
|
11249
|
+
lines.push("export type Blocks = Schema['blocks'];");
|
|
11250
|
+
lines.push("export type Story = InferStory<Blocks>;");
|
|
11251
|
+
lines.push("export type StoryMapi = InferStoryMapi<Blocks>;");
|
|
11252
|
+
lines.push("");
|
|
11253
|
+
return lines.join("\n");
|
|
11254
|
+
}
|
|
11255
|
+
|
|
11256
|
+
async function writeFileWithDirs(filePath, content) {
|
|
11257
|
+
const dir = dirname(filePath);
|
|
11258
|
+
await mkdir(dir, { recursive: true });
|
|
11259
|
+
await writeFile(filePath, content, "utf-8");
|
|
11260
|
+
}
|
|
11261
|
+
async function writeSchemaFiles(targetPath, components, componentFolders, datasources) {
|
|
11262
|
+
const writtenFiles = [];
|
|
11263
|
+
const groupPathByUuid = buildGroupPathByUuid(componentFolders);
|
|
11264
|
+
const groupPathByComponentName = /* @__PURE__ */ new Map();
|
|
11265
|
+
for (const comp of components) {
|
|
11266
|
+
const segments = comp.component_group_uuid ? groupPathByUuid.get(comp.component_group_uuid) ?? [] : [];
|
|
11267
|
+
if (segments.length > 0) {
|
|
11268
|
+
groupPathByComponentName.set(comp.name, segments);
|
|
11269
|
+
}
|
|
11270
|
+
const fileName = componentFileName(comp.name);
|
|
11271
|
+
const filePath = join(targetPath, "blocks", ...segments, `${fileName}.ts`);
|
|
11272
|
+
await writeFileWithDirs(filePath, generateComponentFile(comp));
|
|
11273
|
+
writtenFiles.push(filePath);
|
|
11274
|
+
}
|
|
11275
|
+
for (const ds of datasources) {
|
|
11276
|
+
const fileName = datasourceFileName(ds);
|
|
11277
|
+
const filePath = join(targetPath, "datasources", `${fileName}.ts`);
|
|
11278
|
+
await writeFileWithDirs(filePath, generateDatasourceFile(ds));
|
|
11279
|
+
writtenFiles.push(filePath);
|
|
11280
|
+
}
|
|
11281
|
+
const schemaPath = join(targetPath, "schema.ts");
|
|
11282
|
+
await writeFileWithDirs(schemaPath, generateSchemaFile(components, datasources, groupPathByComponentName));
|
|
11283
|
+
writtenFiles.push(schemaPath);
|
|
11284
|
+
return writtenFiles;
|
|
11285
|
+
}
|
|
11286
|
+
|
|
11287
|
+
async function isTargetEmpty(targetPath) {
|
|
11288
|
+
try {
|
|
11289
|
+
const entries = await readdir(targetPath);
|
|
11290
|
+
return entries.every((entry) => entry.startsWith("."));
|
|
11291
|
+
} catch (maybeError) {
|
|
11292
|
+
const error = maybeError;
|
|
11293
|
+
if (error?.code === "ENOENT") {
|
|
11294
|
+
return true;
|
|
11295
|
+
}
|
|
11296
|
+
throw error;
|
|
11297
|
+
}
|
|
11298
|
+
}
|
|
11299
|
+
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) => {
|
|
11300
|
+
const ui = getUI();
|
|
11301
|
+
const logger = getLogger();
|
|
11302
|
+
const reporter = getReporter();
|
|
11303
|
+
const { space, verbose } = command.optsWithGlobals();
|
|
11304
|
+
const { state } = session();
|
|
11305
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Initializing schema...");
|
|
11306
|
+
logger.info("Schema init started", { space });
|
|
11307
|
+
if (!requireAuthentication(state, verbose)) {
|
|
11308
|
+
return;
|
|
11309
|
+
}
|
|
11310
|
+
if (!space) {
|
|
11311
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
11312
|
+
return;
|
|
11313
|
+
}
|
|
11314
|
+
const targetPath = resolve(options.outDir);
|
|
11315
|
+
const targetDisplayPath = displayPath(targetPath, options.outDir);
|
|
11316
|
+
if (!await isTargetEmpty(targetPath)) {
|
|
11317
|
+
handleError(
|
|
11318
|
+
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.`),
|
|
11319
|
+
verbose
|
|
11320
|
+
);
|
|
11321
|
+
return;
|
|
11322
|
+
}
|
|
11323
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
11324
|
+
try {
|
|
11325
|
+
const fetchSpinner = ui.createSpinner(`Fetching schema from space ${space}...`);
|
|
11326
|
+
let fetchResult;
|
|
11327
|
+
try {
|
|
11328
|
+
fetchResult = await fetchRemoteSchema(space);
|
|
11329
|
+
} catch (maybeError) {
|
|
11330
|
+
fetchSpinner.failed("Failed to fetch remote schema");
|
|
11331
|
+
handleError(toError(maybeError), verbose);
|
|
11332
|
+
return;
|
|
11333
|
+
}
|
|
11334
|
+
const { rawComponents, rawComponentFolders, rawDatasources } = fetchResult;
|
|
11335
|
+
fetchSpinner.succeed(`Found: ${rawComponents.length} components, ${rawComponentFolders.length} component folders, ${rawDatasources.length} datasources`);
|
|
11336
|
+
const writeSpinner = ui.createSpinner(`Generating TypeScript files to ${targetDisplayPath}...`);
|
|
11337
|
+
const writtenFiles = await writeSchemaFiles(targetPath, rawComponents, rawComponentFolders, rawDatasources);
|
|
11338
|
+
summary.total = writtenFiles.length;
|
|
11339
|
+
summary.succeeded = writtenFiles.length;
|
|
11340
|
+
writeSpinner.succeed(`Generated ${writtenFiles.length} files`);
|
|
11341
|
+
ui.list(writtenFiles.map((file) => displayPath(file, options.outDir)));
|
|
11342
|
+
ui.warn("`schema init` is a one-time bootstrap step for adopting an existing space. Review generated files before continuing.");
|
|
11343
|
+
ui.info("After bootstrapping, keep your local schema as the source of truth and use `schema push` for ongoing changes.");
|
|
11344
|
+
ui.info("Make sure `@storyblok/schema` is installed in the project that imports these files (e.g. `pnpm add @storyblok/schema`).");
|
|
11345
|
+
} catch (maybeError) {
|
|
11346
|
+
summary.failed += 1;
|
|
11347
|
+
handleError(toError(maybeError), verbose);
|
|
11348
|
+
} finally {
|
|
11349
|
+
logger.info("Schema init finished", { summary });
|
|
11350
|
+
reporter.addSummary("schemaInitResults", summary);
|
|
11351
|
+
reporter.finalize();
|
|
11352
|
+
}
|
|
11353
|
+
});
|
|
11354
|
+
|
|
11355
|
+
const API_ASSIGNED_FIELDS = [
|
|
11356
|
+
"id",
|
|
11357
|
+
"created_at",
|
|
11358
|
+
"updated_at",
|
|
11359
|
+
"real_name",
|
|
11360
|
+
"all_presets",
|
|
11361
|
+
"image",
|
|
11362
|
+
"uuid"
|
|
11363
|
+
];
|
|
11364
|
+
function stripApiFields(payload) {
|
|
11365
|
+
const result = { ...payload };
|
|
11366
|
+
for (const field of API_ASSIGNED_FIELDS) {
|
|
11367
|
+
delete result[field];
|
|
11368
|
+
}
|
|
11369
|
+
return result;
|
|
11370
|
+
}
|
|
11371
|
+
async function listChangesets(basePath) {
|
|
11372
|
+
const dir = join(basePath, "schema", "changesets");
|
|
11373
|
+
if (!await fileExists(dir)) {
|
|
11374
|
+
return [];
|
|
11375
|
+
}
|
|
11376
|
+
const files = await readDirectory(dir);
|
|
11377
|
+
return files.filter((f) => f.endsWith(".json")).sort().reverse().map((f) => join(dir, f));
|
|
11378
|
+
}
|
|
11379
|
+
async function loadChangeset(filePath) {
|
|
11380
|
+
const content = await readFile$1(filePath, "utf-8");
|
|
11381
|
+
return JSON.parse(content);
|
|
11382
|
+
}
|
|
11383
|
+
function buildRollbackOps(changeset) {
|
|
11384
|
+
if (changeset.changes.length === 0) {
|
|
11385
|
+
return [];
|
|
11386
|
+
}
|
|
11387
|
+
return changeset.changes.map((entry) => {
|
|
11388
|
+
switch (entry.action) {
|
|
11389
|
+
case "create":
|
|
11390
|
+
return { type: entry.type, name: entry.name, action: "delete", payload: {} };
|
|
11391
|
+
case "update":
|
|
11392
|
+
return { type: entry.type, name: entry.name, action: "update", payload: entry.before ?? {} };
|
|
11393
|
+
case "delete":
|
|
11394
|
+
return { type: entry.type, name: entry.name, action: "create", payload: entry.before ?? {} };
|
|
11395
|
+
default:
|
|
11396
|
+
return { type: entry.type, name: entry.name, action: entry.action, payload: {} };
|
|
11397
|
+
}
|
|
11398
|
+
});
|
|
11399
|
+
}
|
|
11400
|
+
function rollbackAction(original) {
|
|
11401
|
+
switch (original) {
|
|
11402
|
+
case "create":
|
|
11403
|
+
return "delete";
|
|
11404
|
+
case "update":
|
|
11405
|
+
return "update";
|
|
11406
|
+
case "delete":
|
|
11407
|
+
return "create";
|
|
11408
|
+
}
|
|
11409
|
+
}
|
|
11410
|
+
function formatRollbackOutput(changes) {
|
|
11411
|
+
const byType = {
|
|
11412
|
+
component: [],
|
|
11413
|
+
datasource: []
|
|
11414
|
+
};
|
|
11415
|
+
for (const entry of changes) {
|
|
11416
|
+
byType[entry.type]?.push(entry);
|
|
11417
|
+
}
|
|
11418
|
+
const icons = {
|
|
11419
|
+
create: chalk.green("+"),
|
|
11420
|
+
update: chalk.yellow("~"),
|
|
11421
|
+
delete: chalk.red("-")
|
|
11422
|
+
};
|
|
11423
|
+
const lines = [];
|
|
11424
|
+
const sections = [
|
|
11425
|
+
["Components", byType.component],
|
|
11426
|
+
["Datasources", byType.datasource]
|
|
11427
|
+
];
|
|
11428
|
+
for (const [label, entries] of sections) {
|
|
11429
|
+
if (entries.length === 0) {
|
|
11430
|
+
continue;
|
|
11431
|
+
}
|
|
11432
|
+
lines.push(chalk.bold(label));
|
|
11433
|
+
for (const entry of entries) {
|
|
11434
|
+
const action = rollbackAction(entry.action);
|
|
11435
|
+
const icon = icons[action] ?? " ";
|
|
11436
|
+
const name = action === "delete" ? chalk.red(entry.name) : entry.name;
|
|
11437
|
+
lines.push(` ${icon} ${name} ${chalk.dim(`(${action})`)}`);
|
|
11438
|
+
if (entry.action === "update" && entry.before && entry.after) {
|
|
11439
|
+
let fromStr;
|
|
11440
|
+
let toStr;
|
|
11441
|
+
if (entry.type === "component") {
|
|
11442
|
+
fromStr = serializeComponent(applyDefaults(entry.after, COMPONENT_DEFAULTS));
|
|
11443
|
+
toStr = serializeComponent(applyDefaults(entry.before, COMPONENT_DEFAULTS));
|
|
11444
|
+
} else {
|
|
11445
|
+
fromStr = serializeDatasource(entry.after);
|
|
11446
|
+
toStr = serializeDatasource(entry.before);
|
|
11447
|
+
}
|
|
11448
|
+
if (fromStr !== toStr) {
|
|
11449
|
+
const patch = createTwoFilesPatch(
|
|
11450
|
+
`current/${entry.name}`,
|
|
11451
|
+
`restore/${entry.name}`,
|
|
11452
|
+
fromStr,
|
|
11453
|
+
toStr,
|
|
11454
|
+
"current",
|
|
11455
|
+
"restore"
|
|
11456
|
+
);
|
|
11457
|
+
for (const line of patch.split("\n")) {
|
|
11458
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
11459
|
+
lines.push(` ${chalk.green(line)}`);
|
|
11460
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
11461
|
+
lines.push(` ${chalk.red(line)}`);
|
|
11462
|
+
}
|
|
11463
|
+
}
|
|
11464
|
+
}
|
|
11465
|
+
}
|
|
11466
|
+
}
|
|
11467
|
+
lines.push("");
|
|
11468
|
+
}
|
|
11469
|
+
return lines.join("\n").trimEnd();
|
|
11470
|
+
}
|
|
11471
|
+
async function executeRollback(spaceId, ops, remote) {
|
|
11472
|
+
const client = getMapiClient();
|
|
11473
|
+
const spaceIdNum = Number(spaceId);
|
|
11474
|
+
let created = 0;
|
|
11475
|
+
let updated = 0;
|
|
11476
|
+
let deleted = 0;
|
|
11477
|
+
const componentOps = ops.filter((op) => op.type === "component");
|
|
11478
|
+
const datasourceOps = ops.filter((op) => op.type === "datasource");
|
|
11479
|
+
for (const op of componentOps.filter((o) => o.action !== "delete")) {
|
|
11480
|
+
if (op.action === "create") {
|
|
11481
|
+
const payload = toComponentCreate(stripApiFields(op.payload));
|
|
11482
|
+
try {
|
|
11483
|
+
await client.components.create({
|
|
11484
|
+
path: { space_id: spaceIdNum },
|
|
11485
|
+
body: { component: payload },
|
|
11486
|
+
throwOnError: true
|
|
11487
|
+
});
|
|
11488
|
+
created++;
|
|
11489
|
+
} catch (error) {
|
|
11490
|
+
handleAPIError("push_component", error, `Failed to create component ${op.name}`);
|
|
11491
|
+
}
|
|
11492
|
+
} else if (op.action === "update") {
|
|
11493
|
+
const existing = remote.components.get(op.name);
|
|
11494
|
+
if (existing?.id) {
|
|
11495
|
+
const payload = toComponentUpdate(stripApiFields(op.payload));
|
|
11496
|
+
try {
|
|
11497
|
+
await client.components.update(existing.id, {
|
|
11498
|
+
path: { space_id: spaceIdNum },
|
|
11499
|
+
body: { component: payload },
|
|
11500
|
+
throwOnError: true
|
|
11501
|
+
});
|
|
11502
|
+
updated++;
|
|
11503
|
+
} catch (error) {
|
|
11504
|
+
handleAPIError("update_component", error, `Failed to update component ${op.name}`);
|
|
11505
|
+
}
|
|
11506
|
+
}
|
|
11507
|
+
}
|
|
11508
|
+
}
|
|
11509
|
+
for (const op of datasourceOps.filter((o) => o.action !== "delete")) {
|
|
11510
|
+
if (op.action === "create") {
|
|
11511
|
+
const payload = toDatasourceCreate(stripApiFields(op.payload));
|
|
11512
|
+
try {
|
|
11513
|
+
await client.datasources.create({
|
|
11514
|
+
path: { space_id: spaceIdNum },
|
|
11515
|
+
body: { datasource: payload },
|
|
11516
|
+
throwOnError: true
|
|
11517
|
+
});
|
|
11518
|
+
created++;
|
|
11519
|
+
} catch (error) {
|
|
11520
|
+
handleAPIError("push_datasource", error, `Failed to create datasource ${op.name}`);
|
|
11521
|
+
}
|
|
11522
|
+
} else if (op.action === "update") {
|
|
11523
|
+
const existing = remote.datasources.get(op.name);
|
|
11524
|
+
if (existing?.id) {
|
|
11525
|
+
const payload = toDatasourceUpdate(stripApiFields(op.payload), existing);
|
|
11526
|
+
try {
|
|
11527
|
+
await client.datasources.update(existing.id, {
|
|
11528
|
+
path: { space_id: spaceIdNum },
|
|
11529
|
+
body: { datasource: payload },
|
|
11530
|
+
throwOnError: true
|
|
11531
|
+
});
|
|
11532
|
+
updated++;
|
|
11533
|
+
} catch (error) {
|
|
11534
|
+
handleAPIError("update_datasource", error, `Failed to update datasource ${op.name}`);
|
|
11535
|
+
}
|
|
11536
|
+
}
|
|
11537
|
+
}
|
|
11538
|
+
}
|
|
11539
|
+
for (const op of datasourceOps.filter((o) => o.action === "delete")) {
|
|
11540
|
+
const existing = remote.datasources.get(op.name);
|
|
11541
|
+
if (existing?.id) {
|
|
11542
|
+
try {
|
|
11543
|
+
await client.datasources.delete(existing.id, {
|
|
11544
|
+
path: { space_id: spaceIdNum },
|
|
11545
|
+
throwOnError: true
|
|
11546
|
+
});
|
|
11547
|
+
deleted++;
|
|
11548
|
+
} catch (error) {
|
|
11549
|
+
handleAPIError("delete_datasource", error, `Failed to delete datasource ${op.name}`);
|
|
11550
|
+
}
|
|
11551
|
+
}
|
|
11552
|
+
}
|
|
11553
|
+
for (const op of componentOps.filter((o) => o.action === "delete")) {
|
|
11554
|
+
const existing = remote.components.get(op.name);
|
|
11555
|
+
if (existing?.id) {
|
|
11556
|
+
try {
|
|
11557
|
+
await client.components.delete(existing.id, {
|
|
11558
|
+
path: { space_id: spaceIdNum },
|
|
11559
|
+
throwOnError: true
|
|
11560
|
+
});
|
|
11561
|
+
deleted++;
|
|
11562
|
+
} catch (error) {
|
|
11563
|
+
handleAPIError("delete_component", error, `Failed to delete component ${op.name}`);
|
|
11564
|
+
}
|
|
11565
|
+
}
|
|
11566
|
+
}
|
|
11567
|
+
return { created, updated, deleted };
|
|
11568
|
+
}
|
|
11569
|
+
|
|
11570
|
+
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) => {
|
|
11571
|
+
const ui = getUI();
|
|
11572
|
+
const logger = getLogger();
|
|
11573
|
+
const reporter = getReporter();
|
|
11574
|
+
const { space, path: basePath, verbose } = command.optsWithGlobals();
|
|
11575
|
+
const { state } = session();
|
|
11576
|
+
ui.title(commands.SCHEMA, colorPalette.SCHEMA, "Rolling back schema...");
|
|
11577
|
+
logger.info("Schema rollback started", { changesetFile, space });
|
|
11578
|
+
if (!requireAuthentication(state, verbose)) {
|
|
11579
|
+
return;
|
|
11580
|
+
}
|
|
11581
|
+
if (!space) {
|
|
11582
|
+
handleError(new CommandError("Please provide the space as argument --space SPACE_ID."), verbose);
|
|
11583
|
+
return;
|
|
11584
|
+
}
|
|
11585
|
+
const summary = { total: 0, succeeded: 0, failed: 0 };
|
|
11586
|
+
try {
|
|
11587
|
+
const resolvedBase = resolvePath(basePath, "");
|
|
11588
|
+
let resolvedFile;
|
|
11589
|
+
if (changesetFile) {
|
|
11590
|
+
resolvedFile = changesetFile;
|
|
11591
|
+
} else if (options.latest) {
|
|
11592
|
+
const available = await listChangesets(resolvedBase);
|
|
11593
|
+
if (available.length === 0) {
|
|
11594
|
+
ui.warn("No changesets found. Run `schema push` first to create one.");
|
|
11595
|
+
return;
|
|
11596
|
+
}
|
|
11597
|
+
resolvedFile = available[0];
|
|
11598
|
+
ui.info(`Using latest changeset: ${basename(resolvedFile)}`);
|
|
11599
|
+
} else {
|
|
11600
|
+
const available = await listChangesets(resolvedBase);
|
|
11601
|
+
if (available.length === 0) {
|
|
11602
|
+
ui.warn("No changesets found. Run `schema push` first to create one.");
|
|
11603
|
+
return;
|
|
11604
|
+
}
|
|
11605
|
+
resolvedFile = await select({
|
|
11606
|
+
message: "Select a changeset to roll back:",
|
|
11607
|
+
choices: available.map((f) => ({ name: basename(f), value: f }))
|
|
11608
|
+
});
|
|
11609
|
+
}
|
|
11610
|
+
let changeset;
|
|
11611
|
+
try {
|
|
11612
|
+
changeset = await loadChangeset(resolvedFile);
|
|
11613
|
+
} catch (maybeError) {
|
|
11614
|
+
handleError(toError(maybeError), verbose);
|
|
11615
|
+
return;
|
|
11616
|
+
}
|
|
11617
|
+
logger.info("Changeset loaded", { file: resolvedFile, changes: changeset.changes.length });
|
|
11618
|
+
const ops = buildRollbackOps(changeset);
|
|
11619
|
+
if (ops.length === 0) {
|
|
11620
|
+
ui.ok("Changeset has no changes \u2014 nothing to roll back.");
|
|
11621
|
+
return;
|
|
11622
|
+
}
|
|
11623
|
+
ui.br();
|
|
11624
|
+
ui.log(formatRollbackOutput(changeset.changes));
|
|
11625
|
+
if (options.dryRun) {
|
|
11626
|
+
ui.info("Dry run \u2014 no changes applied.");
|
|
11627
|
+
logger.info("Dry run completed", { ops: ops.length });
|
|
11628
|
+
return;
|
|
11629
|
+
}
|
|
11630
|
+
if (!options.yes) {
|
|
11631
|
+
const confirmed = await confirm({
|
|
11632
|
+
message: `Apply rollback of ${ops.length} change(s) from ${basename(resolvedFile)}?`,
|
|
11633
|
+
default: false
|
|
11634
|
+
});
|
|
11635
|
+
if (!confirmed) {
|
|
11636
|
+
ui.info("Rollback cancelled.");
|
|
11637
|
+
return;
|
|
11638
|
+
}
|
|
11639
|
+
}
|
|
11640
|
+
const remoteSpinner = ui.createSpinner(`Fetching current remote state from space ${space}...`);
|
|
11641
|
+
let remoteResult;
|
|
11642
|
+
try {
|
|
11643
|
+
remoteResult = await fetchRemoteSchema(space);
|
|
11644
|
+
} catch (maybeError) {
|
|
11645
|
+
remoteSpinner.failed("Failed to fetch remote schema");
|
|
11646
|
+
handleError(toError(maybeError), verbose);
|
|
11647
|
+
return;
|
|
11648
|
+
}
|
|
11649
|
+
const { remote } = remoteResult;
|
|
11650
|
+
remoteSpinner.succeed(`Remote: ${remote.components.size} components, ${remote.componentFolders.size} component folders, ${remote.datasources.size} datasources`);
|
|
11651
|
+
const rollbackSpinner = ui.createSpinner("Applying rollback...");
|
|
11652
|
+
let result;
|
|
11653
|
+
try {
|
|
11654
|
+
result = await executeRollback(space, ops, remote);
|
|
11655
|
+
} catch (error) {
|
|
11656
|
+
rollbackSpinner.failed("Failed to apply rollback");
|
|
11657
|
+
throw error;
|
|
11658
|
+
}
|
|
11659
|
+
summary.total = result.created + result.updated + result.deleted;
|
|
11660
|
+
summary.succeeded = summary.total;
|
|
11661
|
+
rollbackSpinner.succeed(`Rolled back: ${result.created} created, ${result.updated} updated, ${result.deleted} deleted.`);
|
|
11662
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
11663
|
+
const rollbackChangesetPath = await saveChangeset(resolvedBase, {
|
|
11664
|
+
timestamp,
|
|
11665
|
+
spaceId: Number(space),
|
|
11666
|
+
remote: { components: remoteResult.rawComponents, componentFolders: remoteResult.rawComponentFolders, datasources: remoteResult.rawDatasources },
|
|
11667
|
+
changes: ops.map((op) => ({
|
|
11668
|
+
type: op.type,
|
|
11669
|
+
name: op.name,
|
|
11670
|
+
action: op.action,
|
|
11671
|
+
...Object.keys(op.payload).length > 0 && { after: op.payload }
|
|
11672
|
+
}))
|
|
11673
|
+
});
|
|
11674
|
+
logger.info("Rollback changeset saved", { path: displayPath(rollbackChangesetPath, basePath) });
|
|
11675
|
+
} catch (maybeError) {
|
|
11676
|
+
summary.failed += 1;
|
|
11677
|
+
handleError(toError(maybeError), verbose);
|
|
11678
|
+
} finally {
|
|
11679
|
+
logger.info("Schema rollback finished", { summary });
|
|
11680
|
+
reporter.addSummary("schemaRollbackResults", summary);
|
|
11681
|
+
reporter.finalize();
|
|
11682
|
+
}
|
|
11683
|
+
});
|
|
11684
|
+
|
|
9785
11685
|
const program = getProgram();
|
|
9786
11686
|
konsola.br();
|
|
9787
11687
|
konsola.br();
|