storyblok 4.2.2 → 4.3.2

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 CHANGED
@@ -9,8 +9,10 @@ import { Spinner } from '@topcli/spinner';
9
9
  import { select, password, input, confirm } from '@inquirer/prompts';
10
10
  import fs, { mkdir, writeFile, readFile as readFile$1, access, readdir } from 'node:fs/promises';
11
11
  import path, { join, parse, resolve } from 'node:path';
12
+ import filenamify from 'filenamify';
12
13
  import { exec, spawn } from 'node:child_process';
13
14
  import { promisify } from 'node:util';
15
+ import { getRegion } from '@storyblok/region-helper';
14
16
  import { minimatch } from 'minimatch';
15
17
  import { hash } from 'ohash';
16
18
  import { compile } from 'json-schema-to-typescript';
@@ -613,6 +615,11 @@ const resolvePath = (path, folder) => {
613
615
  const getComponentNameFromFilename = (filename) => {
614
616
  return filename.replace(/\.js$/, "");
615
617
  };
618
+ const sanitizeFilename = (filename) => {
619
+ return filenamify(filename, {
620
+ replacement: "_"
621
+ });
622
+ };
616
623
  async function readJsonFile(filePath) {
617
624
  try {
618
625
  const content = (await readFile(filePath)).toString();
@@ -1019,8 +1026,30 @@ program$f.command(commands.USER).description("Get the current user").action(asyn
1019
1026
  konsola.br();
1020
1027
  });
1021
1028
 
1029
+ function getRegionFromSpaceId(spaceId) {
1030
+ try {
1031
+ const region = getRegion(spaceId);
1032
+ return region;
1033
+ } catch (error) {
1034
+ console.warn(`Failed to determine region from space ID: ${error}`);
1035
+ return void 0;
1036
+ }
1037
+ }
1038
+ const resolveRegion = async (thisCommand) => {
1039
+ const options = thisCommand.opts();
1040
+ const spaceId = options.space;
1041
+ if (spaceId) {
1042
+ const { state, initializeSession } = session();
1043
+ await initializeSession();
1044
+ const detectedRegion = getRegionFromSpaceId(spaceId);
1045
+ if (detectedRegion) {
1046
+ state.region = detectedRegion;
1047
+ }
1048
+ }
1049
+ };
1050
+
1022
1051
  const program$e = getProgram();
1023
- const componentsCommand = program$e.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/components");
1052
+ const componentsCommand = program$e.command(commands.COMPONENTS).alias("comp").description(`Manage your space's block schema`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/components").hook("preAction", resolveRegion);
1024
1053
 
1025
1054
  let instance = null;
1026
1055
  const createMapiClient = (options) => {
@@ -1223,11 +1252,12 @@ const saveComponentsToFiles = async (space, spaceData, options) => {
1223
1252
  try {
1224
1253
  if (separateFiles) {
1225
1254
  for (const component of components) {
1226
- const componentFilePath = join(resolvedPath, suffix ? `${component.name}.${suffix}.json` : `${component.name}.json`);
1255
+ const sanitizedName = sanitizeFilename(component.name);
1256
+ const componentFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${suffix}.json` : `${sanitizedName}.json`);
1227
1257
  await saveToFile(componentFilePath, JSON.stringify(component, null, 2));
1228
1258
  const componentPresets = presets.filter((preset) => preset.component_id === component.id);
1229
1259
  if (componentPresets.length > 0) {
1230
- const presetsFilePath = join(resolvedPath, suffix ? `${component.name}.presets.${suffix}.json` : `${component.name}.presets.json`);
1260
+ const presetsFilePath = join(resolvedPath, suffix ? `${sanitizedName}.presets.${suffix}.json` : `${sanitizedName}.presets.json`);
1231
1261
  await saveToFile(presetsFilePath, JSON.stringify(componentPresets, null, 2));
1232
1262
  }
1233
1263
  const groupsFilePath = join(resolvedPath, suffix ? `groups.${suffix}.json` : `groups.json`);
@@ -1569,153 +1599,6 @@ componentsCommand.command("pull [componentName]").option("-f, --filename <filena
1569
1599
  }
1570
1600
  });
1571
1601
 
1572
- const pushDatasource = async (space, datasource) => {
1573
- try {
1574
- const client = mapiClient();
1575
- const { data } = await client.post(`spaces/${space}/datasources`, {
1576
- body: JSON.stringify(datasource)
1577
- });
1578
- return data.datasource;
1579
- } catch (error) {
1580
- handleAPIError("push_datasource", error, `Failed to push datasource ${datasource.name}`);
1581
- }
1582
- };
1583
- const updateDatasource = async (space, datasourceId, datasource) => {
1584
- try {
1585
- const client = mapiClient();
1586
- const { data } = await client.put(`spaces/${space}/datasources/${datasourceId}`, {
1587
- body: JSON.stringify(datasource)
1588
- });
1589
- return data.datasource;
1590
- } catch (error) {
1591
- handleAPIError("update_datasource", error, `Failed to update datasource ${datasource.name}`);
1592
- }
1593
- };
1594
- const upsertDatasource = async (space, datasource, existingId) => {
1595
- if (existingId) {
1596
- return await updateDatasource(space, existingId, datasource);
1597
- } else {
1598
- return await pushDatasource(space, datasource);
1599
- }
1600
- };
1601
- const pushDatasourceEntry = async (space, datasourceId, entry) => {
1602
- try {
1603
- const client = mapiClient();
1604
- const { data } = await client.post(`spaces/${space}/datasource_entries`, {
1605
- body: JSON.stringify({
1606
- datasource_entry: {
1607
- ...entry,
1608
- datasource_id: datasourceId
1609
- }
1610
- })
1611
- });
1612
- return data.datasource_entry;
1613
- } catch (error) {
1614
- handleAPIError("push_datasource", error, `Failed to push datasource entry ${entry.name}`);
1615
- }
1616
- };
1617
- const updateDatasourceEntry = async (space, entryId, entry) => {
1618
- try {
1619
- const client = mapiClient();
1620
- await client.put(`spaces/${space}/datasource_entries/${entryId}`, {
1621
- body: JSON.stringify({
1622
- datasource_entry: entry
1623
- })
1624
- });
1625
- } catch (error) {
1626
- handleAPIError("update_datasource", error, `Failed to update datasource entry ${entry.name}`);
1627
- }
1628
- };
1629
- const upsertDatasourceEntry = async (space, datasourceId, entry, existingId) => {
1630
- if (existingId) {
1631
- await updateDatasourceEntry(space, existingId, entry);
1632
- return void 0;
1633
- } else {
1634
- return await pushDatasourceEntry(space, datasourceId, entry);
1635
- }
1636
- };
1637
- const readDatasourcesFiles = async (options) => {
1638
- const { from, path, separateFiles = false, suffix, space } = options;
1639
- const resolvedPath = resolvePath(path, `datasources/${from}`);
1640
- try {
1641
- await readdir(resolvedPath);
1642
- } catch (error) {
1643
- const message = `No local datasources found for space ${chalk.bold(from)}. To push datasources, you need to pull them first:
1644
-
1645
- 1. Pull the datasources from your source space:
1646
- ${chalk.cyan(`storyblok datasources pull --space ${from}`)}
1647
-
1648
- 2. Then try pushing again:
1649
- ${chalk.cyan(`storyblok datasources push --space ${space} --from ${from}`)}`;
1650
- throw new FileSystemError(
1651
- "file_not_found",
1652
- "read",
1653
- error,
1654
- message
1655
- );
1656
- }
1657
- if (separateFiles) {
1658
- return await readSeparateFiles(resolvedPath, suffix);
1659
- }
1660
- return await readConsolidatedFiles(resolvedPath, suffix);
1661
- };
1662
- async function readSeparateFiles(resolvedPath, suffix) {
1663
- const files = await readdir(resolvedPath);
1664
- const datasources = [];
1665
- const filteredFiles = files.filter((file) => {
1666
- if (suffix) {
1667
- return file.endsWith(`.${suffix}.json`);
1668
- } else {
1669
- return !/\.\w+\.json$/.test(file);
1670
- }
1671
- });
1672
- for (const file of filteredFiles) {
1673
- const filePath = join(resolvedPath, file);
1674
- if (file.endsWith(".json") || file.endsWith(`${suffix}.json`)) {
1675
- if (file === "datasources.json" || /^datasources\.\w+\.json$/.test(file)) {
1676
- continue;
1677
- }
1678
- const result = await readJsonFile(filePath);
1679
- if (result.error) {
1680
- handleFileSystemError("read", result.error);
1681
- continue;
1682
- }
1683
- datasources.push(...result.data);
1684
- }
1685
- }
1686
- return {
1687
- datasources
1688
- };
1689
- }
1690
- async function readConsolidatedFiles(resolvedPath, suffix) {
1691
- const datasourcesPath = join(resolvedPath, suffix ? `datasources.${suffix}.json` : "datasources.json");
1692
- const datasourcesResult = await readJsonFile(datasourcesPath);
1693
- if (datasourcesResult.error || !datasourcesResult.data.length) {
1694
- throw new FileSystemError(
1695
- "file_not_found",
1696
- "read",
1697
- datasourcesResult.error || new Error("Datasources file is empty"),
1698
- `No datasources found in ${datasourcesPath}. Please make sure you have pulled the datasources first.`
1699
- );
1700
- }
1701
- return {
1702
- datasources: datasourcesResult.data
1703
- };
1704
- }
1705
-
1706
- function createStubDatasource(name) {
1707
- return {
1708
- id: 0,
1709
- // Will be set by API
1710
- name,
1711
- slug: name,
1712
- dimensions: [],
1713
- entries: [],
1714
- // Empty entries for stub
1715
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
1716
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
1717
- };
1718
- }
1719
1602
  function buildDependencyGraph(context) {
1720
1603
  const { spaceState } = context;
1721
1604
  const graph = { nodes: /* @__PURE__ */ new Map() };
@@ -1727,22 +1610,6 @@ function buildDependencyGraph(context) {
1727
1610
  dependency.dependents.add(dependentId);
1728
1611
  }
1729
1612
  }
1730
- const referencedDatasources = /* @__PURE__ */ new Set();
1731
- spaceState.local.components.forEach((component) => {
1732
- if (component.schema) {
1733
- const dependencies = collectWhitelistDependencies(component.schema);
1734
- dependencies.datasourceNames.forEach((datasourceName) => {
1735
- referencedDatasources.add(datasourceName);
1736
- });
1737
- }
1738
- });
1739
- referencedDatasources.forEach((datasourceName) => {
1740
- const nodeId = `datasource:${datasourceName}`;
1741
- const targetDatasource = spaceState.target.datasources?.get(datasourceName);
1742
- const stubDatasource = createStubDatasource(datasourceName);
1743
- const node = new DatasourceNode(nodeId, stubDatasource, targetDatasource);
1744
- graph.nodes.set(nodeId, node);
1745
- });
1746
1613
  spaceState.local.internalTags.forEach((tag) => {
1747
1614
  const nodeId = `tag:${tag.id}`;
1748
1615
  const targetTag = spaceState.target.tags.get(tag.name);
@@ -1818,10 +1685,6 @@ function buildDependencyGraph(context) {
1818
1685
  const dependencyId = `component:${componentName}`;
1819
1686
  addDependency(componentId, dependencyId);
1820
1687
  });
1821
- dependencies.datasourceNames.forEach((datasourceName) => {
1822
- const datasourceId = `datasource:${datasourceName}`;
1823
- addDependency(componentId, datasourceId);
1824
- });
1825
1688
  }
1826
1689
  });
1827
1690
  spaceState.local.presets.forEach((preset) => {
@@ -2204,15 +2067,6 @@ class ComponentNode extends GraphNode {
2204
2067
  });
2205
2068
  }
2206
2069
  }
2207
- if ((resolvedField.type === "option" || resolvedField.type === "options") && resolvedField.source === "internal") {
2208
- if (resolvedField.datasource_slug && typeof resolvedField.datasource_slug === "string") {
2209
- const datasourceNodeId = `datasource:${resolvedField.datasource_slug}`;
2210
- const datasourceNode = graph.nodes.get(datasourceNodeId);
2211
- if (datasourceNode?.targetData) {
2212
- resolvedField.datasource_slug = datasourceNode.targetData.resource.slug;
2213
- }
2214
- }
2215
- }
2216
2070
  Object.keys(resolvedField).forEach((key) => {
2217
2071
  if (typeof resolvedField[key] === "object" && resolvedField[key] !== null) {
2218
2072
  resolvedField[key] = resolveField(resolvedField[key]);
@@ -2284,23 +2138,6 @@ class PresetNode {
2284
2138
  };
2285
2139
  }
2286
2140
  }
2287
- class DatasourceNode extends GraphNode {
2288
- constructor(id, data, targetDatasource) {
2289
- super(id, "datasource", data.name, data, targetDatasource);
2290
- }
2291
- resolveReferences(_graph) {
2292
- }
2293
- async upsert(space) {
2294
- const existingDatasource = this.targetData?.resource;
2295
- const existingId = existingDatasource?.id;
2296
- const { entries, ...datasourceDefinition } = this.sourceData;
2297
- const result = await upsertDatasource(space, datasourceDefinition, existingId);
2298
- if (!result) {
2299
- throw new Error(`Failed to upsert datasource ${this.name}`);
2300
- }
2301
- return result;
2302
- }
2303
- }
2304
2141
 
2305
2142
  function collectAllDependencies(components, allComponents, allGroups, allTags) {
2306
2143
  const requiredComponents = /* @__PURE__ */ new Set();
@@ -2701,63 +2538,6 @@ async function pushWithDependencyGraph(space, spaceState, maxConcurrency = 5) {
2701
2538
  return results;
2702
2539
  }
2703
2540
 
2704
- const fetchDatasourceEntries = async (space, datasourceId) => {
2705
- try {
2706
- const client = mapiClient();
2707
- const { data } = await client.get(`spaces/${space}/datasource_entries?datasource_id=${datasourceId}`);
2708
- return data.datasource_entries;
2709
- } catch (error) {
2710
- handleAPIError("pull_datasources", error);
2711
- }
2712
- };
2713
- const fetchDatasources = async (space) => {
2714
- try {
2715
- const client = mapiClient();
2716
- const { data } = await client.get(`spaces/${space}/datasources`);
2717
- const datasources = data.datasources;
2718
- const datasourcesWithEntries = await Promise.all(
2719
- datasources.map(async (ds) => {
2720
- const entries = await fetchDatasourceEntries(space, ds.id);
2721
- return { ...ds, entries };
2722
- })
2723
- );
2724
- return datasourcesWithEntries;
2725
- } catch (error) {
2726
- handleAPIError("pull_datasources", error);
2727
- }
2728
- };
2729
- const fetchDatasource = async (space, datasourceName) => {
2730
- try {
2731
- const client = mapiClient();
2732
- const { data } = await client.get(`spaces/${space}/datasources?search=${encodeURIComponent(datasourceName)}`);
2733
- const found = data.datasources?.find((d) => d.name === datasourceName);
2734
- if (!found) {
2735
- return void 0;
2736
- }
2737
- const entries = await fetchDatasourceEntries(space, found.id);
2738
- return { ...found, entries };
2739
- } catch (error) {
2740
- handleAPIError("pull_datasources", error, `Failed to fetch datasource ${datasourceName}`);
2741
- }
2742
- };
2743
- const saveDatasourcesToFiles = async (space, datasources, options) => {
2744
- const { filename = "datasources", suffix, path, separateFiles } = options;
2745
- const resolvedPath = path ? resolve(process.cwd(), path, "datasources", space) : resolvePath(path, `datasources/${space}`);
2746
- try {
2747
- if (separateFiles) {
2748
- for (const datasource of datasources) {
2749
- const datasourceFilePath = join(resolvedPath, suffix ? `${datasource.name}.${suffix}.json` : `${datasource.name}.json`);
2750
- await saveToFile(datasourceFilePath, JSON.stringify(datasource, null, 2));
2751
- }
2752
- return;
2753
- }
2754
- const datasourcesFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
2755
- await saveToFile(datasourcesFilePath, JSON.stringify(datasources, null, 2));
2756
- } catch (error) {
2757
- handleFileSystemError("write", error);
2758
- }
2759
- };
2760
-
2761
2541
  const program$c = getProgram();
2762
2542
  componentsCommand.command("push [componentName]").description(`Push your space's components schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the components before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files").option("--su, --suffix <suffix>", "Suffix to add to the component name").action(async (componentName, options) => {
2763
2543
  konsola.title(`${commands.COMPONENTS}`, colorPalette.COMPONENTS, componentName ? `Pushing component ${componentName}...` : "Pushing components...");
@@ -2811,10 +2591,9 @@ componentsCommand.command("push [componentName]").description(`Push your space's
2811
2591
  fetchComponents(space),
2812
2592
  fetchComponentGroups(space),
2813
2593
  fetchComponentPresets(space),
2814
- fetchComponentInternalTags(space),
2815
- fetchDatasources(space)
2594
+ fetchComponentInternalTags(space)
2816
2595
  ];
2817
- const [components, groups, presets, internalTags, datasources] = await Promise.all(promises);
2596
+ const [components, groups, presets, internalTags] = await Promise.all(promises);
2818
2597
  if (components) {
2819
2598
  components.forEach((component) => {
2820
2599
  spaceState.target.components.set(component.name, component);
@@ -2839,11 +2618,6 @@ componentsCommand.command("push [componentName]").description(`Push your space's
2839
2618
  spaceState.target.tags.set(tag.name, tag);
2840
2619
  });
2841
2620
  }
2842
- if (datasources) {
2843
- datasources.forEach((datasource) => {
2844
- spaceState.target.datasources.set(datasource.name, datasource);
2845
- });
2846
- }
2847
2621
  if (componentName) {
2848
2622
  spaceState.local = filterSpaceDataByComponent(spaceState.local, componentName);
2849
2623
  if (!spaceState.local.components.length) {
@@ -2881,6 +2655,26 @@ componentsCommand.command("push [componentName]").description(`Push your space's
2881
2655
  }
2882
2656
  }
2883
2657
  console.log(`${requestCount} requests made`);
2658
+ const referencedDatasources = /* @__PURE__ */ new Set();
2659
+ spaceState.local.components.forEach((component) => {
2660
+ if (component.schema) {
2661
+ const fields = JSON.stringify(component.schema);
2662
+ const datasourceMatches = fields.match(/"datasource_slug"\s*:\s*"([^"]+)"/g);
2663
+ if (datasourceMatches) {
2664
+ datasourceMatches.forEach((match) => {
2665
+ const slug = match.match(/"([^"]+)"$/)?.[1];
2666
+ if (slug) {
2667
+ referencedDatasources.add(slug);
2668
+ }
2669
+ });
2670
+ }
2671
+ }
2672
+ });
2673
+ if (referencedDatasources.size > 0) {
2674
+ konsola.br();
2675
+ konsola.info(`Components reference datasources: ${chalk.yellow(Array.from(referencedDatasources).join(", "))}`);
2676
+ konsola.info(`To manage datasources, use: ${chalk.cyan("storyblok datasources push")}`);
2677
+ }
2884
2678
  } catch (error) {
2885
2679
  handleError(error, verbose);
2886
2680
  }
@@ -2916,7 +2710,7 @@ const saveLanguagesToFile = async (space, internationalizationOptions, options)
2916
2710
  };
2917
2711
 
2918
2712
  const program$b = getProgram();
2919
- const languagesCommand = program$b.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/languages");
2713
+ const languagesCommand = program$b.command(commands.LANGUAGES).alias("lang").description(`Manage your space's languages`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/languages").hook("preAction", resolveRegion);
2920
2714
  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.").action(async (options) => {
2921
2715
  konsola.title(`${commands.LANGUAGES}`, colorPalette.LANGUAGES);
2922
2716
  const verbose = program$b.opts().verbose;
@@ -2961,7 +2755,7 @@ languagesCommand.command("pull").description(`Download your space's languages sc
2961
2755
  });
2962
2756
 
2963
2757
  const program$a = getProgram();
2964
- const migrationsCommand = program$a.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/migrations");
2758
+ const migrationsCommand = program$a.command(commands.MIGRATIONS).alias("mig").description(`Manage your space's migrations`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/migrations").hook("preAction", resolveRegion);
2965
2759
 
2966
2760
  const getMigrationTemplate = () => {
2967
2761
  return `export default function (block) {
@@ -3034,9 +2828,9 @@ migrationsCommand.command("generate [componentName]").description("Generate a mi
3034
2828
  }
3035
2829
  });
3036
2830
 
3037
- const fetchStories = async (space, token, region, params) => {
2831
+ const fetchStories = async (space, params) => {
3038
2832
  try {
3039
- const url = getStoryblokUrl(region);
2833
+ const client = mapiClient();
3040
2834
  const allStories = [];
3041
2835
  let currentPage = 1;
3042
2836
  let hasMorePages = true;
@@ -3047,14 +2841,10 @@ const fetchStories = async (space, token, region, params) => {
3047
2841
  ...currentPage > 1 && { page: currentPage.toString() }
3048
2842
  }).toString();
3049
2843
  const queryString = filter_query ? `${regularParams ? `${regularParams}&` : ""}${filter_query}` : regularParams;
3050
- const endpoint = `${url}/spaces/${space}/stories${queryString ? `?${queryString}` : ""}`;
3051
- const response = await customFetch(endpoint, {
3052
- headers: {
3053
- Authorization: token
3054
- }
3055
- });
3056
- allStories.push(...response.stories);
3057
- const totalPages = Math.ceil(response.total / response.perPage);
2844
+ const endpoint = `spaces/${space}/stories${queryString ? `?${queryString}` : ""}`;
2845
+ const { data } = await client.get(endpoint, {});
2846
+ allStories.push(...data.stories);
2847
+ const totalPages = Math.ceil(data.total / data.per_page);
3058
2848
  hasMorePages = currentPage < totalPages;
3059
2849
  currentPage++;
3060
2850
  }
@@ -3064,7 +2854,7 @@ const fetchStories = async (space, token, region, params) => {
3064
2854
  }
3065
2855
  };
3066
2856
  async function fetchStoriesByComponent(spaceOptions, filterOptions) {
3067
- const { spaceId, token, region } = spaceOptions;
2857
+ const { spaceId } = spaceOptions;
3068
2858
  const { componentName = "", query, starts_with } = filterOptions || {};
3069
2859
  const params = {
3070
2860
  ...starts_with && { starts_with }
@@ -3076,39 +2866,30 @@ async function fetchStoriesByComponent(spaceOptions, filterOptions) {
3076
2866
  params.filter_query = query.startsWith("filter_query") ? query : `filter_query${query}`;
3077
2867
  }
3078
2868
  try {
3079
- const stories = await fetchStories(spaceId, token, region, params);
2869
+ const stories = await fetchStories(spaceId, params);
3080
2870
  return stories ?? [];
3081
2871
  } catch (error) {
3082
2872
  handleAPIError("pull_stories", error);
3083
2873
  }
3084
2874
  }
3085
- const fetchStory = async (space, token, region, storyId) => {
2875
+ const fetchStory = async (space, storyId) => {
3086
2876
  try {
3087
- const url = getStoryblokUrl(region);
3088
- const endpoint = `${url}/spaces/${space}/stories/${storyId}`;
3089
- const response = await customFetch(endpoint, {
3090
- headers: {
3091
- Authorization: token
3092
- }
3093
- });
3094
- return response.story;
2877
+ const client = mapiClient();
2878
+ const endpoint = `spaces/${space}/stories/${storyId}`;
2879
+ const { data } = await client.get(endpoint, {});
2880
+ return data.story;
3095
2881
  } catch (error) {
3096
2882
  handleAPIError("pull_story", error);
3097
2883
  }
3098
2884
  };
3099
- const updateStory = async (space, token, region, storyId, payload) => {
2885
+ const updateStory = async (space, storyId, payload) => {
3100
2886
  try {
3101
- const url = getStoryblokUrl(region);
3102
- const endpoint = `${url}/spaces/${space}/stories/${storyId}`;
3103
- const response = await customFetch(endpoint, {
3104
- method: "PUT",
3105
- headers: {
3106
- "Authorization": token,
3107
- "Content-Type": "application/json"
3108
- },
2887
+ const client = mapiClient();
2888
+ const endpoint = `spaces/${space}/stories/${storyId}`;
2889
+ const { data } = await client.put(endpoint, {
3109
2890
  body: JSON.stringify(payload)
3110
2891
  });
3111
- return response.story;
2892
+ return data.story;
3112
2893
  } catch (error) {
3113
2894
  handleAPIError("update_story", error);
3114
2895
  }
@@ -3424,6 +3205,10 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3424
3205
  return;
3425
3206
  }
3426
3207
  const { password, region } = state;
3208
+ mapiClient({
3209
+ token: password,
3210
+ region
3211
+ });
3427
3212
  try {
3428
3213
  const spinner = new Spinner({
3429
3214
  verbose: !isVitest
@@ -3448,9 +3233,7 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3448
3233
  const storiesSpinner = new Spinner({ verbose: !isVitest }).start(`Fetching stories...`);
3449
3234
  const stories = await fetchStoriesByComponent(
3450
3235
  {
3451
- spaceId: space,
3452
- token: password,
3453
- region
3236
+ spaceId: space
3454
3237
  },
3455
3238
  // Filter options
3456
3239
  {
@@ -3464,7 +3247,7 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3464
3247
  return;
3465
3248
  }
3466
3249
  const storiesWithContent = await Promise.all(stories.map(async (story) => {
3467
- const fullStory = await fetchStory(space, password, region, story.id.toString());
3250
+ const fullStory = await fetchStory(space, story.id.toString());
3468
3251
  return {
3469
3252
  ...story,
3470
3253
  content: fullStory?.content
@@ -3536,7 +3319,7 @@ migrationsCommand.command("run [componentName]").description("Run migrations").o
3536
3319
  payload.publish = 1;
3537
3320
  }
3538
3321
  try {
3539
- const updatedStory = await updateStory(space, password, region, story.id, payload);
3322
+ const updatedStory = await updateStory(space, story.id, payload);
3540
3323
  if (updatedStory) {
3541
3324
  successCount++;
3542
3325
  storySpinner.succeed(`Updated story ${chalk.hex(colorPalette.PRIMARY)(story.name || story.id.toString())} - Completed in ${storySpinner.elapsedTime.toFixed(2)}ms`);
@@ -3608,7 +3391,7 @@ migrationsCommand.command("rollback [migrationFile]").description("Rollback a mi
3608
3391
  });
3609
3392
 
3610
3393
  const program$6 = getProgram();
3611
- const typesCommand = program$6.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/types");
3394
+ const typesCommand = program$6.command(commands.TYPES).alias("ts").description(`Generate types d.ts for your component schemas`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/types").hook("preAction", resolveRegion);
3612
3395
 
3613
3396
  const getAssetJSONSchema = (title) => ({
3614
3397
  $id: "#/asset",
@@ -4034,7 +3817,7 @@ const DEFAULT_TYPEDEFS_HEADER = [
4034
3817
  "// This file was generated by the storyblok CLI.",
4035
3818
  "// DO NOT MODIFY THIS FILE BY HAND."
4036
3819
  ];
4037
- const getPropertyTypeAnnotation = (property, prefix) => {
3820
+ const getPropertyTypeAnnotation = (property, prefix, suffix) => {
4038
3821
  if (Array.from(storyblokSchemas.keys()).includes(property.type)) {
4039
3822
  return { type: property.type };
4040
3823
  }
@@ -4047,11 +3830,11 @@ const getPropertyTypeAnnotation = (property, prefix) => {
4047
3830
  if (property.filter_content_type) {
4048
3831
  if (typeof property.filter_content_type === "string") {
4049
3832
  return {
4050
- tsType: `(${getStoryType(property.filter_content_type, prefix)} | string )${property.type === "options" ? "[]" : ""}`
3833
+ tsType: `(${getStoryType(property.filter_content_type, prefix, suffix)} | string )${property.type === "options" ? "[]" : ""}`
4051
3834
  };
4052
3835
  }
4053
3836
  return {
4054
- tsType: `(${property.filter_content_type.map((type2) => getStoryType(type2, prefix)).join(" | ")} | string )${property.type === "options" ? "[]" : ""}`
3837
+ tsType: `(${property.filter_content_type.map((type2) => getStoryType(type2, prefix, suffix)).join(" | ")} | string )${property.type === "options" ? "[]" : ""}`
4055
3838
  };
4056
3839
  }
4057
3840
  }
@@ -4106,13 +3889,14 @@ const getPropertyTypeAnnotation = (property, prefix) => {
4106
3889
  return { type: "any" };
4107
3890
  }
4108
3891
  };
4109
- function getStoryType(property, prefix) {
4110
- return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase(property))}>`;
3892
+ function getStoryType(property, prefix, suffix) {
3893
+ return `${STORY_TYPE}<${prefix ?? ""}${capitalize(toCamelCase(property))}${suffix ?? ""}>`;
4111
3894
  }
4112
3895
  const getComponentType = (componentName, options) => {
4113
3896
  const prefix = options.typePrefix ?? "";
3897
+ const suffix = options.typeSuffix ?? "";
4114
3898
  const sanitizedName = componentName.replace(/[^a-z0-9]/gi, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
4115
- const componentType = toPascalCase(toCamelCase(`${prefix}_${sanitizedName}`));
3899
+ const componentType = toPascalCase(toCamelCase(`${prefix}_${sanitizedName}_${suffix}`));
4116
3900
  const isFirstCharacterNumber = !Number.isNaN(Number.parseInt(componentType.charAt(0)));
4117
3901
  return isFirstCharacterNumber ? `_${componentType}` : componentType;
4118
3902
  };
@@ -4124,7 +3908,7 @@ const getComponentPropertiesTypeAnnotations = async (component, options, spaceDa
4124
3908
  }
4125
3909
  const propertyType = value.type;
4126
3910
  const propertyTypeAnnotation = {
4127
- [key]: getPropertyTypeAnnotation(value, options.typePrefix)
3911
+ [key]: getPropertyTypeAnnotation(value, options.typePrefix, options.typeSuffix)
4128
3912
  };
4129
3913
  if (propertyType === "custom" && customFieldsParser) {
4130
3914
  const customField = typeof customFieldsParser === "function" ? customFieldsParser(key, value) : {};
@@ -4163,6 +3947,15 @@ const getComponentPropertiesTypeAnnotations = async (component, options, spaceDa
4163
3947
  );
4164
3948
  propertyTypeAnnotation[key].tsType = componentsInGroupWhitelist.length > 0 ? `(${componentsInGroupWhitelist.join(" | ")})[]` : `never[]`;
4165
3949
  }
3950
+ } else if (value.restrict_type === "tags") {
3951
+ if (Array.isArray(value.component_tag_whitelist) && value.component_tag_whitelist.length > 0) {
3952
+ const componentsWithTags = spaceData.components.filter(
3953
+ (component2) => component2.internal_tag_ids && component2.internal_tag_ids.some(
3954
+ (tagId) => value.component_tag_whitelist.includes(Number(tagId))
3955
+ )
3956
+ );
3957
+ propertyTypeAnnotation[key].tsType = componentsWithTags.length > 0 ? `(${componentsWithTags.map((component2) => getComponentType(component2.name, options)).join(" | ")})[]` : `never[]`;
3958
+ }
4166
3959
  } else {
4167
3960
  if (Array.isArray(value.component_whitelist) && value.component_whitelist.length > 0) {
4168
3961
  propertyTypeAnnotation[key].tsType = `(${value.component_whitelist.map((name) => getComponentType(name, options)).join(" | ")})[]`;
@@ -4308,7 +4101,7 @@ const generateStoryblokTypes = async (options = {}) => {
4308
4101
  };
4309
4102
 
4310
4103
  const program$5 = getProgram();
4311
- typesCommand.command("generate").description("Generate types d.ts for your component schemas").option("--sf, --separate-files", "").option("--strict", "strict mode, no loose typing").option("--type-prefix <prefix>", "prefix to be prepended to all generated component type names").option("--suffix <suffix>", "Components suffix").option("--custom-fields-parser <path>", "Path to the parser file for Custom Field Types").option("--compiler-options <options>", "path to the compiler options from json-schema-to-typescript").action(async (options) => {
4104
+ typesCommand.command("generate").description("Generate types d.ts for your component schemas").option("--sf, --separate-files", "").option("--strict", "strict mode, no loose typing").option("--type-prefix <prefix>", "prefix to be prepended to all generated component type names").option("--type-suffix <suffix>", "suffix to be appended to all generated component type names").option("--suffix <suffix>", "Components suffix").option("--custom-fields-parser <path>", "Path to the parser file for Custom Field Types").option("--compiler-options <options>", "path to the compiler options from json-schema-to-typescript").action(async (options) => {
4312
4105
  konsola.title(`${commands.TYPES}`, colorPalette.TYPES, "Generating types...");
4313
4106
  const verbose = program$5.opts().verbose;
4314
4107
  const { space, path } = typesCommand.opts();
@@ -4351,7 +4144,65 @@ typesCommand.command("generate").description("Generate types d.ts for your compo
4351
4144
  });
4352
4145
 
4353
4146
  const program$4 = getProgram();
4354
- const datasourcesCommand = program$4.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/datasources");
4147
+ const datasourcesCommand = program$4.command(commands.DATASOURCES).alias("ds").description(`Manage your space's datasources`).option("-s, --space <space>", "space ID").option("-p, --path <path>", "path to save the file. Default is .storyblok/datasources").hook("preAction", resolveRegion);
4148
+
4149
+ const fetchDatasourceEntries = async (space, datasourceId) => {
4150
+ try {
4151
+ const client = mapiClient();
4152
+ const { data } = await client.get(`spaces/${space}/datasource_entries?datasource_id=${datasourceId}`);
4153
+ return data.datasource_entries;
4154
+ } catch (error) {
4155
+ handleAPIError("pull_datasources", error);
4156
+ }
4157
+ };
4158
+ const fetchDatasources = async (space) => {
4159
+ try {
4160
+ const client = mapiClient();
4161
+ const { data } = await client.get(`spaces/${space}/datasources`);
4162
+ const datasources = data.datasources;
4163
+ const datasourcesWithEntries = await Promise.all(
4164
+ datasources.map(async (ds) => {
4165
+ const entries = await fetchDatasourceEntries(space, ds.id);
4166
+ return { ...ds, entries };
4167
+ })
4168
+ );
4169
+ return datasourcesWithEntries;
4170
+ } catch (error) {
4171
+ handleAPIError("pull_datasources", error);
4172
+ }
4173
+ };
4174
+ const fetchDatasource = async (space, datasourceName) => {
4175
+ try {
4176
+ const client = mapiClient();
4177
+ const { data } = await client.get(`spaces/${space}/datasources?search=${encodeURIComponent(datasourceName)}`);
4178
+ const found = data.datasources?.find((d) => d.name === datasourceName);
4179
+ if (!found) {
4180
+ return void 0;
4181
+ }
4182
+ const entries = await fetchDatasourceEntries(space, found.id);
4183
+ return { ...found, entries };
4184
+ } catch (error) {
4185
+ handleAPIError("pull_datasources", error, `Failed to fetch datasource ${datasourceName}`);
4186
+ }
4187
+ };
4188
+ const saveDatasourcesToFiles = async (space, datasources, options) => {
4189
+ const { filename = "datasources", suffix, path, separateFiles } = options;
4190
+ const resolvedPath = path ? resolve(process.cwd(), path, "datasources", space) : resolvePath(path, `datasources/${space}`);
4191
+ try {
4192
+ if (separateFiles) {
4193
+ for (const datasource of datasources) {
4194
+ const sanitizedName = sanitizeFilename(datasource.name);
4195
+ const datasourceFilePath = join(resolvedPath, suffix ? `${sanitizedName}.${suffix}.json` : `${sanitizedName}.json`);
4196
+ await saveToFile(datasourceFilePath, JSON.stringify(datasource, null, 2));
4197
+ }
4198
+ return;
4199
+ }
4200
+ const datasourcesFilePath = join(resolvedPath, suffix ? `${filename}.${suffix}.json` : `${filename}.json`);
4201
+ await saveToFile(datasourcesFilePath, JSON.stringify(datasources, null, 2));
4202
+ } catch (error) {
4203
+ handleFileSystemError("write", error);
4204
+ }
4205
+ };
4355
4206
 
4356
4207
  const program$3 = getProgram();
4357
4208
  datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <filename>", "custom name to be used in file(s) name instead of space id").option("--sf, --separate-files", "Argument to create a single file for each datasource").option("--su, --suffix <suffix>", "suffix to add to the file name (e.g. datasources.<suffix>.json)").description("Pull datasources from your space").action(async (datasourceName, options) => {
@@ -4423,6 +4274,140 @@ datasourcesCommand.command("pull [datasourceName]").option("-f, --filename <file
4423
4274
  }
4424
4275
  });
4425
4276
 
4277
+ const pushDatasource = async (space, datasource) => {
4278
+ try {
4279
+ const client = mapiClient();
4280
+ const { data } = await client.post(`spaces/${space}/datasources`, {
4281
+ body: JSON.stringify(datasource)
4282
+ });
4283
+ return data.datasource;
4284
+ } catch (error) {
4285
+ handleAPIError("push_datasource", error, `Failed to push datasource ${datasource.name}`);
4286
+ }
4287
+ };
4288
+ const updateDatasource = async (space, datasourceId, datasource) => {
4289
+ try {
4290
+ const client = mapiClient();
4291
+ const { data } = await client.put(`spaces/${space}/datasources/${datasourceId}`, {
4292
+ body: JSON.stringify(datasource)
4293
+ });
4294
+ return data.datasource;
4295
+ } catch (error) {
4296
+ handleAPIError("update_datasource", error, `Failed to update datasource ${datasource.name}`);
4297
+ }
4298
+ };
4299
+ const upsertDatasource = async (space, datasource, existingId) => {
4300
+ if (existingId) {
4301
+ return await updateDatasource(space, existingId, datasource);
4302
+ } else {
4303
+ return await pushDatasource(space, datasource);
4304
+ }
4305
+ };
4306
+ const pushDatasourceEntry = async (space, datasourceId, entry) => {
4307
+ try {
4308
+ const client = mapiClient();
4309
+ const { data } = await client.post(`spaces/${space}/datasource_entries`, {
4310
+ body: JSON.stringify({
4311
+ datasource_entry: {
4312
+ ...entry,
4313
+ datasource_id: datasourceId
4314
+ }
4315
+ })
4316
+ });
4317
+ return data.datasource_entry;
4318
+ } catch (error) {
4319
+ handleAPIError("push_datasource", error, `Failed to push datasource entry ${entry.name}`);
4320
+ }
4321
+ };
4322
+ const updateDatasourceEntry = async (space, entryId, entry) => {
4323
+ try {
4324
+ const client = mapiClient();
4325
+ await client.put(`spaces/${space}/datasource_entries/${entryId}`, {
4326
+ body: JSON.stringify({
4327
+ datasource_entry: entry
4328
+ })
4329
+ });
4330
+ } catch (error) {
4331
+ handleAPIError("update_datasource", error, `Failed to update datasource entry ${entry.name}`);
4332
+ }
4333
+ };
4334
+ const upsertDatasourceEntry = async (space, datasourceId, entry, existingId) => {
4335
+ if (existingId) {
4336
+ await updateDatasourceEntry(space, existingId, entry);
4337
+ return void 0;
4338
+ } else {
4339
+ return await pushDatasourceEntry(space, datasourceId, entry);
4340
+ }
4341
+ };
4342
+ const readDatasourcesFiles = async (options) => {
4343
+ const { from, path, separateFiles = false, suffix, space } = options;
4344
+ const resolvedPath = resolvePath(path, `datasources/${from}`);
4345
+ try {
4346
+ await readdir(resolvedPath);
4347
+ } catch (error) {
4348
+ const message = `No local datasources found for space ${chalk.bold(from)}. To push datasources, you need to pull them first:
4349
+
4350
+ 1. Pull the datasources from your source space:
4351
+ ${chalk.cyan(`storyblok datasources pull --space ${from}`)}
4352
+
4353
+ 2. Then try pushing again:
4354
+ ${chalk.cyan(`storyblok datasources push --space ${space} --from ${from}`)}`;
4355
+ throw new FileSystemError(
4356
+ "file_not_found",
4357
+ "read",
4358
+ error,
4359
+ message
4360
+ );
4361
+ }
4362
+ if (separateFiles) {
4363
+ return await readSeparateFiles(resolvedPath, suffix);
4364
+ }
4365
+ return await readConsolidatedFiles(resolvedPath, suffix);
4366
+ };
4367
+ async function readSeparateFiles(resolvedPath, suffix) {
4368
+ const files = await readdir(resolvedPath);
4369
+ const datasources = [];
4370
+ const filteredFiles = files.filter((file) => {
4371
+ if (suffix) {
4372
+ return file.endsWith(`.${suffix}.json`);
4373
+ } else {
4374
+ return !/\.\w+\.json$/.test(file);
4375
+ }
4376
+ });
4377
+ for (const file of filteredFiles) {
4378
+ const filePath = join(resolvedPath, file);
4379
+ if (file.endsWith(".json") || file.endsWith(`${suffix}.json`)) {
4380
+ if (file === "datasources.json" || /^datasources\.\w+\.json$/.test(file)) {
4381
+ continue;
4382
+ }
4383
+ const result = await readJsonFile(filePath);
4384
+ if (result.error) {
4385
+ handleFileSystemError("read", result.error);
4386
+ continue;
4387
+ }
4388
+ datasources.push(...result.data);
4389
+ }
4390
+ }
4391
+ return {
4392
+ datasources
4393
+ };
4394
+ }
4395
+ async function readConsolidatedFiles(resolvedPath, suffix) {
4396
+ const datasourcesPath = join(resolvedPath, suffix ? `datasources.${suffix}.json` : "datasources.json");
4397
+ const datasourcesResult = await readJsonFile(datasourcesPath);
4398
+ if (datasourcesResult.error || !datasourcesResult.data.length) {
4399
+ throw new FileSystemError(
4400
+ "file_not_found",
4401
+ "read",
4402
+ datasourcesResult.error || new Error("Datasources file is empty"),
4403
+ `No datasources found in ${datasourcesPath}. Please make sure you have pulled the datasources first.`
4404
+ );
4405
+ }
4406
+ return {
4407
+ datasources: datasourcesResult.data
4408
+ };
4409
+ }
4410
+
4426
4411
  const program$2 = getProgram();
4427
4412
  datasourcesCommand.command("push [datasourceName]").description(`Push your space's datasources schema as json`).option("-f, --from <from>", "source space id").option("--fi, --filter <filter>", "glob filter to apply to the datasources before pushing").option("--sf, --separate-files", "Read from separate files instead of consolidated files").option("--su, --suffix <suffix>", "Suffix to add to the datasource name").action(async (datasourceName, options) => {
4428
4413
  konsola.title(`${commands.DATASOURCES}`, colorPalette.DATASOURCES, datasourceName ? `Pushing datasource ${datasourceName}...` : "Pushing datasources...");
@@ -4872,7 +4857,7 @@ program$1.command(`${commands.CREATE} [project-path]`).alias("c").description(`S
4872
4857
  konsola.br();
4873
4858
  });
4874
4859
 
4875
- const version = "4.2.2";
4860
+ const version = "4.3.2";
4876
4861
  const pkg = {
4877
4862
  version: version};
4878
4863
 
@@ -4887,7 +4872,8 @@ program.helpOption("-h, --help", "Display help for command");
4887
4872
  program.on("command:*", () => {
4888
4873
  console.error(`Invalid command: ${program.args.join(" ")}`);
4889
4874
  konsola.br();
4890
- program.help();
4875
+ program.outputHelp();
4876
+ process.exit(1);
4891
4877
  });
4892
4878
  try {
4893
4879
  program.parse(process.argv);