UNPKG

@comic-vine/client

Version:

A JS/TS client for the Comic Vine API

1,233 lines (1,183 loc) 38.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var axios = require('axios'); var zod = require('zod'); var crypto = require('crypto'); function _interopDefault(e) { return e && e.__esModule ? e : { default: e }; } var axios__default = /*#__PURE__*/ _interopDefault(axios); var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for('Symbol.' + name); var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value, }) : (obj[key] = value); var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; var __await = function (promise, isYieldStar) { this[0] = promise; this[1] = isYieldStar; }; var __asyncGenerator = (__this, __arguments, generator) => { var resume = (k, v, yes, no) => { try { var x = generator[k](v), isAwait = (v = x.value) instanceof __await, done = x.done; Promise.resolve(isAwait ? v[0] : v) .then((y) => isAwait ? resume( k === 'return' ? k : 'next', v[1] ? { done: y.done, value: y.value } : y, yes, no, ) : yes({ value: y, done }), ) .catch((e) => resume('throw', e, yes, no)); } catch (e) { no(e); } }, method = (k) => (it[k] = (x) => new Promise((yes, no) => resume(k, x, yes, no))), it = {}; return ( (generator = generator.apply(__this, __arguments)), (it[__knownSymbol('asyncIterator')] = () => it), method('next'), method('throw'), method('return'), it ); }; // src/errors/base-error.ts var BaseError = class extends Error { constructor(details) { super(details.message); this.name = this.constructor.name; if (Error.captureStackTrace) { Error.captureStackTrace(this, this.constructor); } this.help = details.help; } }; // src/errors/generic-error.ts var GenericError = class extends BaseError { constructor(message) { super({ message: `An unexpected error occurred: ${message || 'Unknown Error'}`, help: 'Please open a Github issue with steps to reproduce: https://github.com/AllyMurray/comic-vine/issues', }); } }; // src/errors/custom-error.ts var customError = (error) => { if (error instanceof Error) { return new GenericError(error.message); } return new GenericError(); }; // src/errors/filter-error.ts var ComicVineFilterError = class extends BaseError { constructor() { super({ message: 'There was a problem trying to filter the API results', help: 'Please open a Github issue with steps to reproduce: https://github.com/AllyMurray/comic-vine/issues', }); } }; // src/errors/generic-request-error.ts var ComicVineGenericError = class extends BaseError { constructor(message) { super({ message: `Request to comic vine failed: ${message != null ? message : 'Unknown Error'}`, help: 'Please open a Github issue with steps to reproduce: https://github.com/AllyMurray/comic-vine/issues', }); } }; var ComicVineGenericRequestError = class extends ComicVineGenericError { constructor(message) { super(message); } }; // src/errors/jsonp-callback-missing-error.ts var ComicJsonpCallbackMissingError = class extends BaseError { constructor() { super({ message: 'JSONP format requires a callback', help: 'This library does not use JSONP, please open a Github issue with steps to reproduce: https://github.com/AllyMurray/comic-vine/issues', }); } }; // src/errors/object-not-found-error.ts var ComicVineObjectNotFoundError = class extends BaseError { constructor() { super({ message: 'The requested resource could not be found in the Comic Vine API', help: 'Ensure you have used a valid resource Id', }); } }; // src/errors/options-validation-error.ts var OptionsValidationError = class extends BaseError { constructor(path, message) { super({ message: `Property: ${path.join('.')}, Problem: ${message}`, help: 'If the error message does not provide enough information or you believe there is a bug, please open a Github issue with steps to reproduce: https://github.com/AllyMurray/comic-vine/issues', }); } }; // src/errors/subscriber-only-error.ts var ComicVineSubscriberOnlyError = class extends BaseError { constructor() { super({ message: 'The requested video is for subscribers only', help: 'Subscriber videos are part of a paid service, if you wish to upgrade you can do so here: https://comicvine.gamespot.com/upgrade/', }); } }; // src/errors/unauthorized-error.ts var ComicVineUnauthorizedError = class extends BaseError { constructor() { super({ message: 'Unauthorized response received when calling the Comic Vine API', help: 'Ensure you have a valid API key, you can get one from: https://comicvine.gamespot.com/api/', }); } }; // src/errors/url-format-error.ts var ComicVineUrlFormatError = class extends BaseError { constructor() { super({ message: 'The url for the request was not in the correct format', help: 'Please open a Github issue with steps to reproduce: https://github.com/AllyMurray/comic-vine/issues', }); } }; var AdaptiveConfigSchema = zod.z .object({ monitoringWindowMs: zod.z .number() .positive() .default(15 * 60 * 1e3), // 15 minutes highActivityThreshold: zod.z.number().min(0).default(10), // requests per window moderateActivityThreshold: zod.z.number().min(0).default(3), recalculationIntervalMs: zod.z.number().positive().default(3e4), // 30 seconds sustainedInactivityThresholdMs: zod.z .number() .positive() .default(30 * 60 * 1e3), // 30 minutes backgroundPauseOnIncreasingTrend: zod.z.boolean().default(true), maxUserScaling: zod.z.number().positive().default(2), // don't exceed 2x capacity minUserReserved: zod.z.number().min(0).default(5), // requests minimum }) .refine( (data) => { return data.moderateActivityThreshold < data.highActivityThreshold; }, { message: 'moderateActivityThreshold must be less than highActivityThreshold', }, ); function hashRequest(endpoint, params = {}) { const requestString = JSON.stringify({ endpoint, params: sortObject(params), }); return crypto.createHash('sha256').update(requestString).digest('hex'); } function sortObject(obj) { if (obj === null) { return null; } const objType = typeof obj; if (objType === 'undefined' || objType === 'string') { return obj; } if (objType === 'number' || objType === 'boolean') { return String(obj); } if (Array.isArray(obj)) { return obj.map(sortObject); } const sorted = {}; const keys = Object.keys(obj).sort(); for (const key of keys) { const value = obj[key]; const normalisedValue = sortObject(value); if (normalisedValue !== void 0) { sorted[key] = normalisedValue; } } return sorted; } // src/stores/rate-limit-config.ts var DEFAULT_RATE_LIMIT = { limit: 60, windowMs: 6e4, }; // src/stores/adaptive-capacity-calculator.ts var AdaptiveCapacityCalculator = class { constructor(config = {}) { this.config = AdaptiveConfigSchema.parse(config); } calculateDynamicCapacity(resource, totalLimit, activityMetrics) { const recentUserActivity = this.getRecentActivity( activityMetrics.recentUserRequests, ); const activityTrend = this.calculateActivityTrend( activityMetrics.recentUserRequests, ); if (recentUserActivity >= this.config.highActivityThreshold) { const userCapacity = Math.min( totalLimit * 0.9, Math.floor(totalLimit * 0.5 * this.config.maxUserScaling), // 50% base * scaling factor ); return { userReserved: userCapacity, backgroundMax: totalLimit - userCapacity, backgroundPaused: this.config.backgroundPauseOnIncreasingTrend && activityTrend === 'increasing', reason: `High user activity (${recentUserActivity} requests/${this.config.monitoringWindowMs / 6e4}min) - prioritizing users`, }; } if (recentUserActivity >= this.config.moderateActivityThreshold) { const userMultiplier = this.getUserMultiplier( recentUserActivity, activityTrend, ); const baseUserCapacity2 = Math.floor(totalLimit * 0.4); const dynamicUserCapacity = Math.min( totalLimit * 0.7, baseUserCapacity2 * userMultiplier, ); return { userReserved: dynamicUserCapacity, backgroundMax: totalLimit - dynamicUserCapacity, backgroundPaused: false, reason: `Moderate user activity - dynamic scaling (${userMultiplier.toFixed(1)}x user capacity)`, }; } if (recentUserActivity === 0) { if ( activityMetrics.recentUserRequests.length === 0 && activityMetrics.recentBackgroundRequests.length === 0 ) { const baseUserCapacity2 = Math.floor(totalLimit * 0.3); return { userReserved: Math.max( baseUserCapacity2, this.config.minUserReserved, ), backgroundMax: totalLimit - Math.max(baseUserCapacity2, this.config.minUserReserved), backgroundPaused: false, reason: 'Initial state - default capacity allocation', }; } if (activityMetrics.recentUserRequests.length === 0) { return { userReserved: this.config.minUserReserved, // Minimal safety buffer backgroundMax: totalLimit - this.config.minUserReserved, backgroundPaused: false, reason: 'No user activity yet - background scale up with minimal user buffer', }; } const sustainedInactivity = this.getSustainedInactivityPeriod( activityMetrics.recentUserRequests, ); if (sustainedInactivity > this.config.sustainedInactivityThresholdMs) { return { userReserved: 0, // No reservation - background gets everything! backgroundMax: totalLimit, // Full capacity available backgroundPaused: false, reason: `Sustained zero activity (${Math.floor(sustainedInactivity / 6e4)}+ min) - full capacity to background`, }; } else { return { userReserved: this.config.minUserReserved, // Minimal safety buffer backgroundMax: totalLimit - this.config.minUserReserved, backgroundPaused: false, reason: 'Recent zero activity - background scale up with minimal user buffer', }; } } const baseUserCapacity = Math.floor(totalLimit * 0.3); return { userReserved: Math.max(baseUserCapacity, this.config.minUserReserved), backgroundMax: totalLimit - Math.max(baseUserCapacity, this.config.minUserReserved), backgroundPaused: false, reason: `Low user activity (${recentUserActivity} requests/${this.config.monitoringWindowMs / 6e4}min) - background scale up`, }; } getRecentActivity(requests) { const cutoff = Date.now() - this.config.monitoringWindowMs; return requests.filter((timestamp) => timestamp > cutoff).length; } calculateActivityTrend(requests) { const now = Date.now(); const windowSize = this.config.monitoringWindowMs / 3; const recent = requests.filter((t) => t > now - windowSize).length; const previous = requests.filter( (t) => t > now - 2 * windowSize && t <= now - windowSize, ).length; if (recent === 0 && previous === 0) return 'none'; if (recent > previous * 1.5) return 'increasing'; if (recent < previous * 0.5) return 'decreasing'; return 'stable'; } getUserMultiplier(activity, trend) { let base = Math.min( this.config.maxUserScaling, 1 + activity / this.config.highActivityThreshold, ); if (trend === 'increasing') base *= 1.2; if (trend === 'decreasing') base *= 0.8; return Math.max(1, base); } getSustainedInactivityPeriod(requests) { if (requests.length === 0) { return 0; } const lastRequest = Math.max(...requests); return Date.now() - lastRequest; } }; // src/utils/is-object.ts var isObject = (maybeObject) => maybeObject === Object(maybeObject) && !Array.isArray(maybeObject) && typeof maybeObject !== 'function'; // src/utils/case-converter.ts var toCamelCase = (str) => { return str.replace(/([-_][a-z])/gi, ($1) => { return $1.toUpperCase().replace('-', '').replace('_', ''); }); }; var toSnakeCase = (str) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); var convertCase = (caseConverter, object) => { if (isObject(object)) { const newObject = {}; Object.keys(object).forEach((key) => { newObject[caseConverter(key)] = convertCase(caseConverter, object[key]); }); return newObject; } else if (Array.isArray(object)) { return object.map((arrayElement) => convertCase(caseConverter, arrayElement), ); } return object; }; var convertSnakeCaseToCamelCase = (object) => { return convertCase(toCamelCase, object); }; var convertCamelCaseToSnakeCase = (object) => { return convertCase(toSnakeCase, object); }; // src/http-client/http-client.ts function wait(ms, signal) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { if (signal) { signal.removeEventListener('abort', onAbort); } resolve(); }, ms); function onAbort() { clearTimeout(timer); const err = new Error('Aborted'); err.name = 'AbortError'; reject(err); } if (signal) { if (signal.aborted) { onAbort(); } else { signal.addEventListener('abort', onAbort, { once: true }); } } }); } var HttpClient = class { constructor(stores = {}, options2 = {}) { var _a, _b, _c; this._http = axios__default.default.create(); this.stores = stores; this.options = { defaultCacheTTL: (_a = options2.defaultCacheTTL) != null ? _a : 3600, // 1 hour throwOnRateLimit: (_b = options2.throwOnRateLimit) != null ? _b : true, maxWaitTime: (_c = options2.maxWaitTime) != null ? _c : 6e4, // 1 minute }; } /** * Infer the resource name from the endpoint URL * @param url The full URL or endpoint path * @returns The resource name for rate limiting */ inferResource(url) { const match = url.match(/\/api\/([^/]+)\//); return match ? match[1] : 'unknown'; } /** * Extract endpoint and params from URL for request hashing * @param url The full URL * @returns Object with endpoint and params for hashing */ parseUrlForHashing(url) { const urlObj = new URL(url); const endpoint = urlObj.pathname.replace('/api/', ''); const params = {}; urlObj.searchParams.forEach((value, key) => { params[key] = value; }); return { endpoint, params }; } /** * Type guard to check if a rate limit store supports adaptive features * @param store The rate limit store to check * @returns True if the store is an adaptive rate limit store */ isAdaptiveRateLimitStore(store) { return 'canProceed' in store && store.canProceed.length >= 2; } handleResponse(response) { switch (response.data.statusCode) { case 104 /* FilterError */: throw new ComicVineFilterError(); case 103 /* JsonpCallbackMissing */: throw new ComicJsonpCallbackMissingError(); case 101 /* ObjectNotFound */: throw new ComicVineObjectNotFoundError(); case 105 /* SubscriberOnlyVideo */: throw new ComicVineSubscriberOnlyError(); case 102 /* UrlFormatError */: throw new ComicVineUrlFormatError(); default: return response.data; } } generateClientError(err) { var _a, _b, _c; if (err instanceof BaseError) { return err; } const error = err; if (((_a = error.response) == null ? void 0 : _a.status) === 401) { return new ComicVineUnauthorizedError(); } const errorMessage = (_c = (_b = error.response) == null ? void 0 : _b.data) == null ? void 0 : _c.message; return new ComicVineGenericRequestError( `${error.message}${errorMessage ? `, ${errorMessage}` : ''}`, ); } get(_0) { return __async(this, arguments, function* (url, options2 = {}) { const { signal, priority = 'background' } = options2; const { endpoint, params } = this.parseUrlForHashing(url); const hash = hashRequest(endpoint, params); const resource = this.inferResource(url); try { if (this.stores.cache) { const cachedResult = yield this.stores.cache.get(hash); if (cachedResult !== void 0) { return cachedResult; } } if (this.stores.dedupe) { const existingResult = yield this.stores.dedupe.waitFor(hash); if (existingResult !== void 0) { return existingResult; } yield this.stores.dedupe.register(hash); } if (this.stores.rateLimit) { const isAdaptive = this.isAdaptiveRateLimitStore( this.stores.rateLimit, ); const canProceed = isAdaptive ? yield this.stores.rateLimit.canProceed(resource, priority) : yield this.stores.rateLimit.canProceed(resource); if (!canProceed) { if (this.options.throwOnRateLimit) { const waitTime = isAdaptive ? yield this.stores.rateLimit.getWaitTime(resource, priority) : yield this.stores.rateLimit.getWaitTime(resource); throw new Error( `Rate limit exceeded for resource '${resource}'. Wait ${waitTime}ms before retrying.`, ); } else { const waitTime = Math.min( isAdaptive ? yield this.stores.rateLimit.getWaitTime(resource, priority) : yield this.stores.rateLimit.getWaitTime(resource), this.options.maxWaitTime, ); if (waitTime > 0) { yield wait(waitTime, signal); } } } } const response = yield this._http.get(url, { signal }); const transformedData = response.data ? convertSnakeCaseToCamelCase(response.data) : void 0; const result = this.handleResponse( __spreadProps(__spreadValues({}, response), { data: transformedData, }), ); if (this.stores.rateLimit) { const isAdaptive = this.isAdaptiveRateLimitStore( this.stores.rateLimit, ); if (isAdaptive) { yield this.stores.rateLimit.record(resource, priority); } else { yield this.stores.rateLimit.record(resource); } } if (this.stores.cache) { yield this.stores.cache.set( hash, result, this.options.defaultCacheTTL, ); } if (this.stores.dedupe) { yield this.stores.dedupe.complete(hash, result); } return result; } catch (error) { if (this.stores.dedupe) { yield this.stores.dedupe.fail(hash, error); } if (error instanceof Error && error.name === 'AbortError') { throw error; } throw this.generateClientError(error); } }); } }; // src/resources/resource-list.ts var resource_list_exports = {}; __export(resource_list_exports, { Character: () => Character, Concept: () => Concept, Episode: () => Episode, Issue: () => Issue, Location: () => Location, Movie: () => Movie, Origin: () => Origin, Person: () => Person, Power: () => Power, Promo: () => Promo, Publisher: () => Publisher, Series: () => Series, StoryArc: () => StoryArc, Team: () => Team, Thing: () => Thing, Video: () => Video, VideoCategory: () => VideoCategory, VideoType: () => VideoType, Volume: () => Volume, }); // src/resources/base-resource.ts var BaseResource = class { constructor(httpClient, urlBuilder) { this.httpClient = httpClient; this.urlBuilder = urlBuilder; } retrieve(id, options2) { return __async(this, null, function* () { const url = this.urlBuilder.retrieve(this.resourceType, id, options2); options2 == null ? void 0 : options2.fieldList; const response = yield this.httpClient.get(url, { priority: options2 == null ? void 0 : options2.priority, }); return response.results; }); } fetchPage(options2) { return __async(this, null, function* () { const url = this.urlBuilder.list(this.resourceType, options2); options2 == null ? void 0 : options2.fieldList; const response = yield this.httpClient.get(url, { priority: options2 == null ? void 0 : options2.priority, }); return { limit: response.limit, numberOfPageResults: response.numberOfPageResults, numberOfTotalResults: response.numberOfTotalResults, offset: response.offset, data: response.results, }; }); } list(options2) { const fetchPage = (opts) => this.fetchPage.call(this, opts); const fetchPagePromise = fetchPage(options2); const asyncIterator = { [Symbol.asyncIterator]() { return __asyncGenerator(this, null, function* () { var _a2; const defaultPageSize = 100; const limit = (_a2 = options2 == null ? void 0 : options2.limit) != null ? _a2 : defaultPageSize; let page = (options2 == null ? void 0 : options2.offset) ? options2.offset / limit + 1 : 1; let hasMoreResults = true; let response = yield new __await(fetchPagePromise); do { for (const resource of response.data) { yield resource; } hasMoreResults = response.limit + response.offset < response.numberOfTotalResults; if (hasMoreResults) { response = yield new __await( fetchPage({ limit, offset: response.numberOfPageResults * page++, }), ); } } while (hasMoreResults); }); }, }; const promiseWithAsyncIterator = Object.assign( fetchPagePromise, asyncIterator, ); return promiseWithAsyncIterator; } }; // src/resources/character/character.ts var Character = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4005 /* Character */; } }; // src/resources/concept/concept.ts var Concept = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4015 /* Concept */; } }; // src/resources/episode/episode.ts var Episode = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4070 /* Episode */; } }; // src/resources/issue/issue.ts var Issue = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4e3 /* Issue */; } }; // src/resources/location/location.ts var Location = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4020 /* Location */; } }; // src/resources/movie/movie.ts var Movie = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4025 /* Movie */; } }; // src/resources/origin/origin.ts var Origin = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4030 /* Origin */; } }; // src/resources/person/person.ts var Person = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4040 /* Person */; } }; // src/resources/power/power.ts var Power = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4035 /* Power */; } }; // src/resources/promo/promo.ts var Promo = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 1700 /* Promo */; } }; // src/resources/publisher/publisher.ts var Publisher = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4010 /* Publisher */; } }; // src/resources/series/series.ts var Series = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4075 /* Series */; } }; // src/resources/story-arc/story-arc.ts var StoryArc = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4045 /* StoryArc */; } }; // src/resources/team/team.ts var Team = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4060 /* Team */; } }; // src/resources/thing/thing.ts var Thing = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4055 /* Thing */; } }; // src/resources/video/video.ts var Video = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 2300 /* Video */; } }; // src/resources/video-category/video-category.ts var VideoCategory = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 2320 /* VideoCategory */; } }; // src/resources/video-type/video-type.ts var VideoType = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 2320 /* VideoType */; } }; // src/resources/volume/volume.ts var Volume = class extends BaseResource { constructor() { super(...arguments); this.resourceType = 4050 /* Volume */; } }; // src/resources/resource-factory.ts var ResourceFactory = class { constructor(httpClient, urlBuilder) { this.httpClient = httpClient; this.urlBuilder = urlBuilder; this._resources = __spreadValues({}, resource_list_exports); } create(name) { if (!this._resources[name]) { throw new Error(`${name} resource not implemented`); } const ResourceClass = this._resources[name]; return new ResourceClass(this.httpClient, this.urlBuilder); } }; // src/resources/resource-map.ts var resourceMap = /* @__PURE__ */ new Map([ [4005 /* Character */, { detailName: 'character', listName: 'characters' }], [4015 /* Concept */, { detailName: 'concept', listName: 'concepts' }], [4070 /* Episode */, { detailName: 'episode', listName: 'episodes' }], [4e3 /* Issue */, { detailName: 'issue', listName: 'issues' }], [4020 /* Location */, { detailName: 'location', listName: 'locations' }], [4025 /* Movie */, { detailName: 'movie', listName: 'movies' }], [4030 /* Origin */, { detailName: 'origin', listName: 'origins' }], [4040 /* Person */, { detailName: 'person', listName: 'people' }], [4035 /* Power */, { detailName: 'power', listName: 'powers' }], [1700 /* Promo */, { detailName: 'promo', listName: 'promos' }], [4010 /* Publisher */, { detailName: 'publisher', listName: 'publishers' }], [4075 /* Series */, { detailName: 'series', listName: 'series_list' }], [4045 /* StoryArc */, { detailName: 'story_arc', listName: 'story_arcs' }], [4060 /* Team */, { detailName: 'team', listName: 'teams' }], [4055 /* Thing */, { detailName: 'object', listName: 'objects' }], [2300 /* Video */, { detailName: 'video', listName: 'videos' }], [ 2320 /* VideoCategory */, { detailName: 'video_category', listName: 'video_categories' }, ], [ 2320 /* VideoCategory */, { detailName: 'video_type', listName: 'video_types' }, ], [4050 /* Volume */, { detailName: 'volume', listName: 'volumes' }], ]); var getResource = (resourceType) => { const resource = resourceMap.get(resourceType); if (!resource) { throw new Error(`Resource type (${resourceType}) not found`); } return resource; }; // src/http-client/url-builder.ts var isDefined = (value) => { return value != null; }; var UrlBuilder = class { constructor(apiKey, baseUrl) { this.apiKey = apiKey; this.baseUrl = baseUrl; } getParam(key, value) { if (value) { return { name: toSnakeCase(key), value }; } return void 0; } getFormatParm() { return { name: 'format', value: `json` }; } getApiKeyParm() { return { name: 'api_key', value: this.apiKey }; } getSortParam(sort) { if (sort) { return { name: 'sort', value: `${sort.field}:${sort.direction}` }; } return void 0; } getLimitParam(limit) { if (limit) { return this.getParam('limit', limit); } return void 0; } getOffsetParam(offset) { if (offset) { return this.getParam('offset', offset); } return void 0; } getFieldListParams(fieldList) { if (fieldList) { return { name: 'field_list', value: fieldList.map((field) => toSnakeCase(String(field))).join(','), }; } return void 0; } getFilterParams(filter) { if (filter) { const snakeCaseFilter = convertCamelCaseToSnakeCase(filter); const filterParams = Object.entries(snakeCaseFilter).map( ([key, value]) => `${key}:${value}`, ); return { name: 'filter', value: filterParams.join(',') }; } return void 0; } buildUrl(urlInput, queryParams) { const url = new URL(urlInput, this.baseUrl); const urlSearchParams = new URLSearchParams( queryParams .filter(isDefined) .map((param) => [param.name, param.value.toString()]), ); url.search = urlSearchParams.toString(); return url.toString(); } /** * @param resourceType A unique identifier for the resource type * @param id A unique identifier for the resource * @returns A url for requesting the resource * @example https://comicvine.gamespot.com/api/issue/4000-719442?format=json&api_key=123abc */ retrieve(resourceType, id, options2) { const resource = getResource(resourceType); const urlInput = `${resource.detailName}/${resourceType}-${id}`; const queryParams = [ this.getFormatParm(), this.getApiKeyParm(), this.getFieldListParams(options2 == null ? void 0 : options2.fieldList), ]; return this.buildUrl(urlInput, queryParams); } /** * @param resourceType A unique identifier for the resource type * @returns A url for requesting a list of resources * @example https://comicvine.gamespot.com/api/issues?format=json&api_key=123abc */ list(resourceType, options2) { const urlInput = getResource(resourceType).listName; const queryParams = [ this.getFormatParm(), this.getApiKeyParm(), this.getLimitParam(options2 == null ? void 0 : options2.limit), this.getOffsetParam(options2 == null ? void 0 : options2.offset), this.getSortParam(options2 == null ? void 0 : options2.sort), this.getFieldListParams(options2 == null ? void 0 : options2.fieldList), this.getFilterParams(options2 == null ? void 0 : options2.filter), ]; return this.buildUrl(urlInput, queryParams); } }; // src/http-client/http-client-factory.ts var HttpClientFactory = class { static createClient(stores = {}, options2 = {}) { return new HttpClient(stores, options2); } static createUrlBuilder(apiKey, baseUrl) { return new UrlBuilder(apiKey, baseUrl); } }; var options = zod.z.object({ /** * The base url for the Comic Vine API. * This could be used to set a proxy when using the library in a browser. * It also ensures that if the comic vine url was to change it wouldn't be a breaking change to the library. * @default https://comicvine.gamespot.com/api/ */ baseUrl: zod.z .string() .url() .optional() .default('https://comicvine.gamespot.com/api/'), }); var loadOptions = (userOptions) => { try { return options.parse(userOptions != null ? userOptions : {}); } catch (error) { if (error instanceof zod.ZodError) { const validationError = error.issues[0]; if (validationError) { throw new OptionsValidationError( validationError.path, validationError.message, ); } throw new OptionsValidationError([], 'Unknown validation error'); } throw customError(error); } }; // src/comic-vine.ts function classNameToPropertyName(className) { if (!className) { return ''; } return className.charAt(0).toLowerCase() + className.slice(1); } var ComicVine = class { /** * Create a new ComicVine client * @param options - Configuration options for the client */ constructor(options2) { this.resourceCache = /* @__PURE__ */ new Map(); const { apiKey, baseUrl, stores = {}, client = {} } = options2; const _options = loadOptions({ baseUrl }); const httpClient = HttpClientFactory.createClient(stores, client); const urlBuilder = HttpClientFactory.createUrlBuilder( apiKey, _options.baseUrl, ); this.resourceFactory = new ResourceFactory(httpClient, urlBuilder); this.stores = stores; this.resourceNames = Object.keys(resource_list_exports); return new Proxy(this, { get(target, prop) { if (typeof prop === 'string' && target.isResourceProperty(prop)) { return target.getResource(prop); } return Reflect.get(target, prop); }, }); } isResourceProperty(prop) { const className = prop.charAt(0).toUpperCase() + prop.slice(1); return this.resourceNames.includes(className); } getResource(propertyName) { if (!this.resourceCache.has(propertyName)) { const className = propertyName.charAt(0).toUpperCase() + propertyName.slice(1); try { const resource = this.resourceFactory.create(className); this.resourceCache.set(propertyName, resource); } catch (error) { throw new Error(`Failed to create resource '${className}': ${error}`); } } return this.resourceCache.get(propertyName); } getAvailableResources() { return this.resourceNames.map((name) => classNameToPropertyName(name)); } hasResource(resourceName) { return this.isResourceProperty(resourceName); } getResourceByName(resourceName) { if (!this.isResourceProperty(resourceName)) { return void 0; } return this.getResource(resourceName); } isResourceLoaded(resourceName) { return this.resourceCache.has(resourceName); } getCacheStats() { const total = this.resourceNames.length; const loaded = this.resourceCache.size; const loadedResources = Array.from(this.resourceCache.keys()); return { total, loaded, loadedResources }; } clearCache() { return __async(this, null, function* () { if (this.stores.cache) { yield this.stores.cache.clear(); } }); } getRateLimitStatus(resourceName) { return __async(this, null, function* () { if (this.stores.rateLimit) { return this.stores.rateLimit.getStatus(resourceName); } return null; }); } resetRateLimit(resourceName) { return __async(this, null, function* () { if (this.stores.rateLimit) { yield this.stores.rateLimit.reset(resourceName); } }); } }; // src/index.ts var index_default = ComicVine; if ( typeof module !== 'undefined' && module.exports && // Only mutate if the export object is mutable and `default` is not already defined. typeof module.exports === 'object' && !Object.prototype.hasOwnProperty.call(module.exports, 'default') ) { module.exports = ComicVine; module.exports.default = ComicVine; module.exports.ComicVine = ComicVine; } exports.AdaptiveCapacityCalculator = AdaptiveCapacityCalculator; exports.AdaptiveConfigSchema = AdaptiveConfigSchema; exports.ComicJsonpCallbackMissingError = ComicJsonpCallbackMissingError; exports.ComicVine = ComicVine; exports.ComicVineFilterError = ComicVineFilterError; exports.ComicVineGenericError = ComicVineGenericError; exports.ComicVineGenericRequestError = ComicVineGenericRequestError; exports.ComicVineObjectNotFoundError = ComicVineObjectNotFoundError; exports.ComicVineSubscriberOnlyError = ComicVineSubscriberOnlyError; exports.ComicVineUnauthorizedError = ComicVineUnauthorizedError; exports.ComicVineUrlFormatError = ComicVineUrlFormatError; exports.DEFAULT_RATE_LIMIT = DEFAULT_RATE_LIMIT; exports.GenericError = GenericError; exports.OptionsValidationError = OptionsValidationError; exports.customError = customError; exports.default = index_default; exports.hashRequest = hashRequest; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map