UNPKG

@jungvonmatt/sb-migrate

Version:

CLI tool for managing Storyblok schema and content migrations

396 lines 13.9 kB
"use strict"; 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