@comic-vine/client
Version:
A JS/TS client for the Comic Vine API
1,233 lines (1,183 loc) • 38.4 kB
JavaScript
'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