@jungvonmatt/sb-migrate
Version:
CLI tool for managing Storyblok schema and content migrations
396 lines • 13.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.api = exports.datasourceEntries = exports.datasources = exports.spaces = exports.components = exports.componentGroups = exports.stories = void 0;
exports.setRequestsPerSecond = setRequestsPerSecond;
exports.getStoryblokClient = getStoryblokClient;
/* eslint-disable @typescript-eslint/no-explicit-any */
const storyblok_js_client_1 = __importDefault(require("storyblok-js-client"));
const config_1 = require("../utils/config");
/**
* Creates a request queue with rate limiting to prevent API rate limiting.
*
* @param {number} [initialRequestsPerSecond=3] - The initial number of requests per second
* @returns {Object} An object with methods to add requests to the queue and set the rate limit
*/
const createRequestQueue = (initialRequestsPerSecond = 3) => {
const queue = [];
let processing = false;
let interval = Math.floor(1000 / initialRequestsPerSecond);
let lastRequestTime = 0;
const setRequestsPerSecond = (requestsPerSecond) => {
if (requestsPerSecond <= 0)
return;
interval = Math.floor(1000 / requestsPerSecond);
};
const processQueue = async () => {
if (processing || queue.length === 0)
return;
processing = true;
while (queue.length > 0) {
const now = Date.now();
const timeSinceLastRequest = now - lastRequestTime;
// Wait if needed to respect the rate limit
if (timeSinceLastRequest < interval) {
const waitTime = interval - timeSinceLastRequest;
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
// Execute the next request
const request = queue.shift();
if (request) {
lastRequestTime = Date.now();
try {
await request();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
}
catch (error) {
// Don't log the error here - let it propagate to the actual handler
}
}
}
processing = false;
};
const add = async (fn) => {
return new Promise((resolve, reject) => {
queue.push(async () => {
try {
const result = await fn();
resolve(result);
return result;
}
catch (error) {
reject(error);
throw error;
}
});
if (!processing) {
processQueue();
}
});
};
return {
add,
setRequestsPerSecond,
};
};
// Create a global request queue instance
const requestQueue = createRequestQueue();
/**
* Sets the number of requests per second to limit API calls
* @param requestsPerSecond Number of requests per second (default: 3)
*/
function setRequestsPerSecond(requestsPerSecond) {
requestQueue.setRequestsPerSecond(requestsPerSecond);
}
/**
* Creates a new instance of the Storyblok client using configuration from either
* the config file (.storyblokrc.json) or environment variables.
*
* @throws {Error} If no OAuth token is found in either the config file or environment variables
* @returns {Promise<StoryblokClient>} A configured Storyblok client instance
*
* @example
* ```typescript
* const client = await getStoryblokClient();
* const { data } = await client.get('stories');
* ```
*/
async function getStoryblokClient() {
// Try to load config from config file first
const config = await (0, config_1.loadConfig)();
// If no config file found, try environment variables
const envConfig = config || (await (0, config_1.loadEnvConfig)());
if (!envConfig.oauthToken) {
throw new Error("No Storyblok OAuth token found. Please run the config command first.");
}
return new storyblok_js_client_1.default({
oauthToken: envConfig.oauthToken,
});
}
/**
* Handles paginated GET requests to the Storyblok API, automatically fetching all pages
* and combining the results into a single response.
*
* @param {StoryblokClient} client - Initialized Storyblok client instance
* @param {PagedOptions} params - Pagination and request parameters
* @param {number} [params.page=1] - Current page number
* @param {number} [params.perPage=100] - Number of items per page (max 100)
* @param {ISbResult | null} [params.aggregatedResponse] - Accumulated response from previous pages
* @param {string} params.url - API endpoint URL
* @param {Record<string, any>} [params.options={}] - Additional query parameters
*
* @returns {Promise<ISbResult>} Combined response with all pages of data aggregated
*
* @example
* ```typescript
* const allStories = await pagedGet(client, {
* url: 'spaces/123/stories',
* options: { sort_by: 'created_at:desc' }
* });
* // Returns all stories across all pages
* ```
*/
const pagedGet = async (client, params) => {
const { url, page = 1, perPage = 100, options = {} } = params;
let aggregatedResponse = params?.aggregatedResponse;
// Use the request queue to throttle requests
const response = await requestQueue.add(() => client.get(url, {
...options,
page,
per_page: Math.min(100, perPage), // Ensure we don't exceed max of 100
}));
if (aggregatedResponse) {
aggregatedResponse.data = {
datasource_entries: [
...(aggregatedResponse?.data?.datasource_entries ?? []),
...(response?.data?.datasource_entries ?? []),
],
stories: [
...(aggregatedResponse?.data?.stories ?? []),
...(response?.data?.stories ?? []),
],
};
}
else {
aggregatedResponse = response;
}
if (response.total > page * perPage) {
return pagedGet(client, {
...params,
page: page + 1,
aggregatedResponse,
});
}
return aggregatedResponse;
};
/**
* Wraps API requests to the Storyblok Management API with proper authentication,
* space context, and automatic pagination for GET requests.
*
* @template T - Type of the response data
* @param {string} [path=""] - API endpoint path (without space prefix)
* @param {RequestMethod} method - HTTP method (get, post, put, delete)
* @param {Record<string, any>} [params={}] - Request parameters or body data
*
* @returns {Promise<IResult<T>>} API response with typed data
* @throws {Error} If authentication fails or API request errors
*
* @example
* ```typescript
* // GET request with automatic pagination
* const { data } = await wrapRequest<{ components: IComponent[] }>(
* 'components/',
* 'get',
* { per_page: 100 }
* );
*
* // POST request
* await wrapRequest<{ component: IComponent }>(
* 'components/',
* 'post',
* { component: newComponent }
* );
* ```
*/
const wrapRequest = async (path = "", method, params = {}) => {
const client = await getStoryblokClient();
const config = await (0, config_1.loadConfig)();
if (method === "get") {
const response = await pagedGet(client, {
url: `spaces/${config?.spaceId}/${path}`,
options: params,
});
return response;
}
// For non-GET requests, use the request queue
const response = await requestQueue.add(() => client[method](`spaces/${config?.spaceId}/${path}`, params));
return response;
};
exports.stories = {
getAll(params = {}) {
return wrapRequest(`stories/`, "get", params);
},
get(id, params = {}) {
return wrapRequest(`stories/${id}`, "get", params);
},
async getBySlug(slug) {
const slugParts = slug.split("/");
const name = slugParts[slugParts.length - 1];
const folder = slugParts.length > 1 ? slugParts.slice(0, -1).join("/") : "";
const params = { per_page: 100 };
if (folder) {
params.starts_with = folder;
}
const response = await exports.stories.getAll(params);
const story = response.data.stories.find((s) => s.slug === name || s.full_slug === slug);
if (!story) {
throw new Error(`Story with slug "${slug}" not found`);
}
if (!story.id) {
throw new Error(`Story with slug "${slug}" has no ID`);
}
return (await exports.stories.get(story.id)).data.story;
},
create(story, params = {}) {
return wrapRequest(`stories/`, "post", {
story,
...params,
});
},
update(story, params = {}) {
return wrapRequest(`stories/${story.id}`, "put", {
story,
...params,
});
},
delete(storyOrId) {
const path = typeof storyOrId === "number"
? `stories/${storyOrId}`
: `stories/${storyOrId.id}`;
return wrapRequest(path, "delete");
},
};
/**
* API methods for managing Storyblok components
*/
exports.componentGroups = {
getAll(params = {}) {
return wrapRequest(`component_groups/`, "get", params);
},
get(id, params = {}) {
return wrapRequest(`component_groups/${id}`, "get", params);
},
create(componentGroup) {
return wrapRequest(`component_groups/`, "post", { component_group: componentGroup });
},
update(componentGroup) {
return wrapRequest(`component_groups/${componentGroup.id}`, "put", { component_group: componentGroup });
},
delete(componentGroupOrId) {
const path = typeof componentGroupOrId === "number" ||
typeof componentGroupOrId === "string"
? `component_groups/${componentGroupOrId}`
: `component_groups/${componentGroupOrId.id}`;
return wrapRequest(path, "delete");
},
};
exports.components = {
getAll(params = {}) {
return wrapRequest(`components/`, "get", params);
},
get(id, params = {}) {
return wrapRequest(`components/${id}`, "get", params);
},
create(component) {
return wrapRequest(`components/`, "post", {
component,
});
},
update(component) {
return wrapRequest(`components/${component.id}`, "put", { component });
},
delete(componentOrId) {
const path = typeof componentOrId === "number" || typeof componentOrId === "string"
? `components/${componentOrId}`
: `components/${componentOrId.id}`;
return wrapRequest(path, "delete");
},
};
/**
* API methods for managing Storyblok spaces
*/
exports.spaces = {
async getSpaceOptions() {
const config = await (0, config_1.loadConfig)();
if (!config?.spaceId) {
throw new Error("Space ID is not set");
}
const response = await wrapRequest(`spaces/${config.spaceId}/`, "get");
return response?.data?.space?.options ?? {};
},
async getSpaceLanguages() {
const config = await (0, config_1.loadConfig)();
if (!config?.spaceId) {
throw new Error("Space ID is not set");
}
const response = await wrapRequest(`spaces/${config.spaceId}/`, "get");
return (response?.data?.space?.languages ||
response.data.space.options.languages ||
[]);
},
};
/**
* API methods for managing Storyblok datasources
*/
exports.datasources = {
getAll(params = {}) {
return wrapRequest(`datasources/`, "get", params);
},
get(id, params = {}) {
return wrapRequest(`datasources/${id}`, "get", params);
},
create(entry) {
return wrapRequest(`datasources/`, "post", {
datasource: entry,
});
},
update(entry) {
return wrapRequest(`datasources/${entry.id}`, "put", {
datasource: entry,
});
},
delete(entryOrId) {
const path = typeof entryOrId === "number" || typeof entryOrId === "string"
? `datasources/${entryOrId}`
: `datasources/${entryOrId.id}`;
return wrapRequest(path, "delete");
},
async has(id) {
const response = await exports.datasources.getAll();
return response.data.datasources.some((datasource) => [datasource.slug, datasource.id].includes(id));
},
};
exports.datasourceEntries = {
getAll(datasource, params = {}) {
if (typeof datasource === "number") {
params.datasource_id = datasource;
}
else if (typeof datasource === "string") {
params.datasource_slug = datasource;
}
else {
params.datasource_id = datasource.id;
}
return wrapRequest(`datasource_entries/`, "get", params);
},
get(id, params = {}) {
return wrapRequest(`datasource_entries/${id}`, "get", params);
},
create(entry) {
return wrapRequest(`datasource_entries/`, "post", {
datasource_entry: entry,
});
},
update(entry) {
return wrapRequest(`datasource_entries/${entry.id}`, "put", {
entry,
});
},
delete(entryOrId) {
const path = typeof entryOrId === "number" || typeof entryOrId === "string"
? `datasource_entries/${entryOrId}`
: `datasource_entries/${entryOrId.id}`;
return wrapRequest(path, "delete");
},
};
exports.api = {
spaces: exports.spaces,
stories: exports.stories,
componentGroups: exports.componentGroups,
components: exports.components,
datasources: exports.datasources,
datasourceEntries: exports.datasourceEntries,
};
//# sourceMappingURL=api.js.map