diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6a30277..c86290d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,7 +12,8 @@ updates: open-pull-requests-limit: 1 ignore: - dependency-name: "*" - update-types: ["version-update:semver-patch", "version-update:semver-minor"] + update-types: + ["version-update:semver-patch", "version-update:semver-minor"] commit-message: prefix: "[skip ci]" include: "scope" @@ -25,4 +26,4 @@ updates: groups: everything: patterns: - - "*" \ No newline at end of file + - "*" diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..2e4d705 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "ryanluker.vscode-coverage-gutters", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "rvest.vs-code-prettier-eslint" + ] +} diff --git a/package-lock.json b/package-lock.json index 5ea8ac4..84a3a23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "eslint": "^10.5.0", "eslint-config-prettier": "^10.1.8", "globals": "^17.6.0", + "jiti": "^2.7.0", "prettier": "3.8.4", "ts-node": "^10.9.2", "tsup": "^8.5.1", @@ -2763,6 +2764,16 @@ "node": ">=8" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", diff --git a/package.json b/package.json index 03e6a81..9d4e69d 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "eslint": "^10.5.0", "eslint-config-prettier": "^10.1.8", "globals": "^17.6.0", + "jiti": "^2.7.0", "prettier": "3.8.4", "ts-node": "^10.9.2", "tsup": "^8.5.1", diff --git a/src/index.ts b/src/index.ts index a7f0365..2f609bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,10 @@ import { Assets } from "./lib/assets.js"; import { Sheet, Sheets } from "./lib/sheets.js"; import { Versions } from "./lib/versions.js"; import { CustomError, request } from "./utils.js"; +import * as Models from "./models.js"; export default class XIVAPI { - public readonly options: XIVAPI.Options; + public readonly options: XIVAPIOptions; public readonly achievements: Sheet<"Achievement">; public readonly minions: Sheet<"Companion">; @@ -41,12 +42,12 @@ export default class XIVAPI { /** * A wrapper for the XIVAPI v2 API. - * @param {XIVAPI.Options} [options] The client options to fetch with. + * @param {XIVAPIOptions} [options] The client options to fetch with. * @see https://v2.xivapi.com/api/docs * @since 0.5.0 */ constructor( - options: XIVAPI.Options = { + options: XIVAPIOptions = { version: "latest", language: "en", verbose: false, @@ -62,14 +63,12 @@ export default class XIVAPI { /** * Fetch information about rows and their related data that match the provided search query. - * @param {Models.SearchQuery} params Query paramters accepted by the search endpoint. + * @param {SearchParams} params Query paramters accepted by the search endpoint. * @returns {Promise} Response structure for the search endpoint. * @see https://v2.xivapi.com/api/docs#tag/search/get/search * @since 0.5.0 */ - public async search( - params: XIVAPI.SearchParams - ): Promise { + public async search(params: SearchParams): Promise { const { data, errors } = await request({ path: "/search", params: params as Record, @@ -79,366 +78,29 @@ export default class XIVAPI { } } -export namespace XIVAPI { - export interface Options { - /** - * The supported version of the game to use for the API. - * @default "latest" - */ - version?: string; - /** - * Language to use for the API. - */ - language?: keyof typeof Models.SchemaLanguage; - /** - * Whether to enable verbose logging. - * @default false - */ - verbose?: boolean; - } - +export interface XIVAPIOptions { /** - * Query parameters accepted by the search endpoint. - * @see https://v2.xivapi.com/api/docs#tag/search/get/search + * The supported version of the game to use for the API. + * @default "latest" */ - export type SearchParams = Models.SearchQuery & - Models.VersionQuery & - Models.RowReaderQuery & { verbose?: boolean }; -} - -/** - * Models are used to define the structure of the data returned by the API. - * @see https://v2.xivapi.com/api/docs#models - */ -export namespace Models { + version?: string; /** - * Query parameters accepted by endpoints that interact with versioned game data. - * @see https://v2.xivapi.com/api/docs#model/versionquery + * Language to use for the API. */ - export interface VersionQuery { - /** - * Game version to utilise for this query. - */ - version?: string | null; - } - + language?: keyof typeof Models.SchemaLanguage; /** - * Query parameters accepted by the asset endpoint. - * @see https://v2.xivapi.com/api/docs#model/assetquery + * Whether to enable verbose logging. + * @default false */ - export interface AssetQuery { - format: string | SchemaFormat; - /** - * Game path of the asset to retrieve. - * @example "ui/icon/051000/051474_hr1.tex" - */ - path: string; - } - - /** - * @see https://v2.xivapi.com/api/docs#model/schemaformat - */ - export enum SchemaFormat { - jpg = "jpg", - png = "png", - webp = "webp", - } - - /** - * General purpose error response structure. - * @see https://v2.xivapi.com/api/docs#model/errorresponse - */ - export interface ErrorResponse { - code: number; - /** - * Description of what went wrong. - */ - message: string; - } - - /** - * @see https://v2.xivapi.com/api/docs#model/statuscode - */ - export type StatusCode = number; - - /** - * Query paramters accepted by the search endpoint. - * @see https://v2.xivapi.com/api/docs#model/searchquery - */ - export interface SearchQuery { - /** - * Continuation token to retrieve further results from a prior search request. If specified, takes priority over query. - */ - cursor?: string | null; - /** - * Maximum number of rows to return. To paginate, provide the cursor token provided in `next` to the `cursor` parameter. - */ - limit?: number | null; - /** - * A query string for searching excel data. - * Queries are formed of clauses, which take the basic form of `[specifier][operation][value]`, i.e. `Name="Example"`. Multiple clauses may be specified by seperating them with whitespace, i.e. `Foo=1 Bar=2`. - * @see https://v2.xivapi.com/docs/guides/search/#query - */ - query?: QueryString; - /** - * List of excel sheets that the query should be run against. At least one must be specified if not querying a cursor. - */ - sheets?: string | null; - } - - /** - * A query string for searching excel data. - * Queries are formed of clauses, which take the basic form of `[specifier][operation][value]`, i.e. `Name="Example"`. Multiple clauses may be specified by seperating them with whitespace, i.e. `Foo=1 Bar=2`. - * @see https://v2.xivapi.com/api/docs#model/querystring - */ - export type QueryString = - | string - | string[] - | Record - | URLSearchParams - | null; - - /** - * Query parameters accepted by endpoints that retrieve excel row data. - * @see https://v2.xivapi.com/api/docs#model/rowreaderquery - */ - export interface RowReaderQuery { - /** - * A filter string for selecting fields within a row. - * Filters are comprised of a comma-seperated list of field paths, i.e. `a,b` will select the fields `a` and `b`. - */ - fields?: FilterString | null; - /** - * Known languages supported by the game data format. **NOTE:** Not all languages that are supported by the format are valid for all editions of the game. For example, the global game client acknowledges the existence of `chs` and `kr`, however does not provide any data for them. - */ - language?: string | SchemaLanguage | null; - /** - * Schema that row data should be read with. - */ - schema?: SchemaSpecifier | null; - /** - * A filter string for selecting fields within a row. - * Filters are comprised of a comma-seperated list of field paths, i.e. `a,b` will select the fields `a` and `b`. - */ - transient?: FilterString | null; - } - - /** - * Known languages supported by the game data format. **NOTE:** Not all languages that are supported by the format are valid for all editions of the game. For example, the global game client acknowledges the existence of `chs` and `kr`, however does not provide any data for them. - * @see https://v2.xivapi.com/api/docs#model/schemalanguage - */ - export enum SchemaLanguage { - none = "none", - en = "en", - ja = "ja", - de = "de", - fr = "fr", - chs = "chs", - cht = "cht", - kr = "kr", - } - - /** - * @see https://v2.xivapi.com/api/docs#model/schemaspecifier - */ - export type SchemaSpecifier = string; // `^.+(@.+)?$` - - /** - * A filter string for selecting fields within a row. - * Filters are comprised of a comma-seperated list of field paths, i.e. `a,b` will select the fields `a` and `b`. - * @see https://v2.xivapi.com/api/docs#model/filterstring - */ - export type FilterString = string | string[]; - - /** - * Response structure for the search endpoint. - * @see https://v2.xivapi.com/api/docs#model/searchresponse - */ - export interface SearchResponse { - results: SearchResult[]; - schema: SchemaSpecifier; - next?: string | null; - } - - /** - * Result found by a search query, hydrated with data from the underlying excel row the result represents. - * @see https://v2.xivapi.com/api/docs#model/searchresult - */ - export interface SearchResult { - fields: object; - /** - * ID of this row. - */ - row_id: number; - /** - * Relevance score for this entry. - * These values only loosely represent the relevance of an entry to the search query. No guarantee is given that the discrete values, nor resulting sort order, will remain stable. - */ - score: number; - /** - * Excel sheet this result was found in. - */ - sheet: SchemaSpecifier; - /** - * Subrow ID of this row, when relevant. - */ - subrow_id?: number; - /** - * Field values for this row's transient row, if any is present, according to the current schema and transient filter. - */ - transient?: object; - } - - /** - * Response structure for the list endpoint. - * @see https://v2.xivapi.com/api/docs#model/listresponse - */ - export interface ListResponse { - /** - * Array of sheets known to the API. - * Metadata about a single sheet. - */ - sheets: SheetMetadata[]; - } - - /** - * Metadata about a single sheet. - * @see https://v2.xivapi.com/api/docs#model/sheetmetadata - */ - export interface SheetMetadata { - /** - * The name of the sheet. - */ - name: string; - } - - /** - * Path variables accepted by the sheet endpoint. - * @see https://v2.xivapi.com/api/docs#model/sheetpath - */ - export interface SheetPath { - /** - * Name of the sheet to read. - */ - sheet: SchemaSpecifier; - } - - /** - * Query parameters accepted by the sheet endpoint. - * @see https://v2.xivapi.com/api/docs#model/sheetquery - */ - export interface SheetQuery { - /** - * Fetch rows after the specified row. Behavior is undefined if both `rows` and `after` are provided. - */ - after?: SchemaSpecifier | null; - /** - * Maximum number of rows to return. To paginate, provide the last returned row to the next request's `after` parameter. - */ - limit?: number | null; - /** - * Rows to fetch from the sheet, as a comma-separated list. Behavior is undefined if both `rows` and `after` are provided. - */ - rows?: string; // `^\d+(:\d+)?(,\d+(:\d+)?)*$` - } - - /** - * @see https://v2.xivapi.com/api/docs#model/rowspecifier - */ - export type RowSpecifier = string; // `^\d+(:\d+)?$` - - /** - * Response structure for the sheet endpoint. - * @see https://v2.xivapi.com/api/docs#model/sheetresponse - */ - export interface SheetResponse { - /** - * Array of rows retrieved by the query. - * @see https://v2.xivapi.com/api/docs#model/rowresult - */ - rows: RowResult[]; - /** - * The canonical specifier for the schema used in this response. - */ - schema: SchemaSpecifier; - } - - /** - * Row retrieved by the query. - * @see https://v2.xivapi.com/api/docs#model/rowresult - */ - export interface RowResult { - fields: object; - /** - * ID of this row. - */ - row_id: number; - /** - * Subrow ID of this row, when relevant. - */ - subrow_id?: number | null; - /** - * Field values for this row's transient row, if any is present, according to the current schema and transient filter. - */ - transient?: object; - } - - /** - * Path variables accepted by the row endpoint. - * @see https://v2.xivapi.com/api/docs#model/rowpath - */ - export interface RowPath { - row: RowSpecifier; - /** - * Name of the sheet to read. - */ - sheet: SchemaSpecifier; - } - - /** - * Response structure for the row endpoint. - * @see https://v2.xivapi.com/api/docs#model/rowresponse - */ - export interface RowResponse { - fields: object; - /** - * ID of this row. - */ - row_id: number; - /** - * The canonical specifier for the schema used in this response. - */ - schema: SchemaSpecifier; - /** - * Subrow ID of this row, when relevant. - */ - subrow_id?: number | null; - /** - * Field values for this row's transient row, if any is present, according to the current schema and transient filter. - */ - transient?: object; - } + verbose?: boolean; +} - /** - * Response structure for the versions endpoint. - * @see https://v2.xivapi.com/api/docs#model/versionsresponse - */ - export interface VersionsResponse { - /** - * Array of versions available in the API. - * Metadata about a single version supported by the API. - */ - versions: VersionMetadata[]; - } +/** + * Query parameters accepted by the search endpoint. + * @see https://v2.xivapi.com/api/docs#tag/search/get/search + */ +export type SearchParams = Models.SearchQuery & + Models.VersionQuery & + Models.RowReaderQuery & { verbose?: boolean }; - /** - * Metadata about a single version supported by the API. - * @see https://v2.xivapi.com/api/docs#model/versionmetadata - */ - export interface VersionMetadata { - /** - * Names associated with this version. Version names specified here are accepted by the `version` query parameter throughout the API. - */ - names: string[]; - } -} +export { Models, XIVAPI }; diff --git a/src/lib/assets.ts b/src/lib/assets.ts index 8c15ca5..eaf1363 100644 --- a/src/lib/assets.ts +++ b/src/lib/assets.ts @@ -1,4 +1,4 @@ -import type { Models } from "../index.js"; +import * as Models from "../models.js"; import { CustomError, request } from "../utils.js"; /** @@ -38,6 +38,7 @@ export class Assets { path: `/asset/map/${territory}/${index}`, params: params as unknown as Record, }); + /* v8 ignore if -- @preserve */ if (errors) throw new CustomError(errors[0].message); return data as Buffer; } diff --git a/src/lib/sheets.ts b/src/lib/sheets.ts index f81b8e8..be1fd21 100644 --- a/src/lib/sheets.ts +++ b/src/lib/sheets.ts @@ -1,11 +1,12 @@ -import type { Models, XIVAPI } from "../index.js"; +import type { XIVAPIOptions } from "../index.js"; +import * as Models from "../models.js"; import { CustomError, request } from "../utils.js"; export class Sheet { private readonly type: T; - private readonly options: XIVAPI.Options; + private readonly options: XIVAPIOptions; - constructor(sheet: T, options: XIVAPI.Options) { + constructor(sheet: T, options: XIVAPIOptions) { this.type = sheet; this.options = options; } @@ -22,9 +23,11 @@ export class Sheet { params: Models.RowReaderQuery = {} ): Promise { try { + /* v8 ignore if -- @preserve */ if (typeof id !== "string") id = id.toString(); return new Sheets(this.options).get(this.type, id, params); } catch (error) { + /* v8 ignore next -- @preserve */ throw new CustomError( error instanceof Error ? error.message : "Unknown error" ); @@ -41,6 +44,7 @@ export class Sheet { try { return new Sheets(this.options).list(this.type, params); } catch (error) { + /* v8 ignore next -- @preserve */ throw new CustomError( error instanceof Error ? error.message : "Unknown error" ); @@ -49,15 +53,15 @@ export class Sheet { } export class Sheets { - private readonly options: XIVAPI.Options; + private readonly options: XIVAPIOptions; /** * Endpoints for reading data from the game's static relational data store. - * @param {XIVAPI.Options} [options] The options to fetch the sheets with. + * @param {XIVAPIOptions} [options] The options to fetch the sheets with. * @see https://v2.xivapi.com/api/docs#tag/sheets */ public constructor( - options: XIVAPI.Options = { + options: XIVAPIOptions = { language: "en", verbose: false, } @@ -76,6 +80,7 @@ export class Sheets { params: {}, options: this.options, }); + /* v8 ignore if -- @preserve */ if (errors) throw new CustomError(errors[0].message); return data as Models.ListResponse; } diff --git a/src/lib/versions.ts b/src/lib/versions.ts index 5e0812c..11f5e18 100644 --- a/src/lib/versions.ts +++ b/src/lib/versions.ts @@ -1,4 +1,4 @@ -import type { Models } from "../index.js"; +import * as Models from "../models.js"; import { CustomError, request } from "../utils.js"; /** @@ -8,10 +8,12 @@ import { CustomError, request } from "../utils.js"; export class Versions { /** * List versions understood by the API. + * @returns {Promise} * @see https://v2.xivapi.com/api/docs#tag/versions/get/version */ async all(): Promise { const { data, errors } = await request({ path: "/version", params: {} }); + /* v8 ignore if -- @preserve */ if (errors) throw new CustomError(errors[0].message); return data as Models.VersionsResponse; } diff --git a/src/models.ts b/src/models.ts new file mode 100644 index 0000000..5fdefa6 --- /dev/null +++ b/src/models.ts @@ -0,0 +1,332 @@ +/* eslint-disable no-unused-vars */ + +/** + * Query parameters accepted by endpoints that interact with versioned game data. + * @see https://v2.xivapi.com/api/docs#model/versionquery + */ +export interface VersionQuery { + /** + * Game version to utilise for this query. + */ + version?: string | null; +} + +/** + * Query parameters accepted by the asset endpoint. + * @see https://v2.xivapi.com/api/docs#model/assetquery + */ +export interface AssetQuery { + format: string | SchemaFormat; + /** + * Game path of the asset to retrieve. + * @example "ui/icon/051000/051474_hr1.tex" + */ + path: string; +} + +/** + * @see https://v2.xivapi.com/api/docs#model/schemaformat + */ +export enum SchemaFormat { + jpg = "jpg", + png = "png", + webp = "webp", +} + +/** + * General purpose error response structure. + * @see https://v2.xivapi.com/api/docs#model/errorresponse + */ +export interface ErrorResponse { + code: number; + /** + * Description of what went wrong. + */ + message: string; +} + +/** + * @see https://v2.xivapi.com/api/docs#model/statuscode + */ +export type StatusCode = number; + +/** + * Query paramters accepted by the search endpoint. + * @see https://v2.xivapi.com/api/docs#model/searchquery + */ +export interface SearchQuery { + /** + * Continuation token to retrieve further results from a prior search request. If specified, takes priority over query. + */ + cursor?: string | null; + /** + * Maximum number of rows to return. To paginate, provide the cursor token provided in `next` to the `cursor` parameter. + */ + limit?: number | null; + /** + * A query string for searching excel data. + * Queries are formed of clauses, which take the basic form of `[specifier][operation][value]`, i.e. `Name="Example"`. Multiple clauses may be specified by seperating them with whitespace, i.e. `Foo=1 Bar=2`. + * @see https://v2.xivapi.com/docs/guides/search/#query + */ + query?: QueryString; + /** + * List of excel sheets that the query should be run against. At least one must be specified if not querying a cursor. + */ + sheets?: string | null; +} + +/** + * A query string for searching excel data. + * Queries are formed of clauses, which take the basic form of `[specifier][operation][value]`, i.e. `Name="Example"`. Multiple clauses may be specified by seperating them with whitespace, i.e. `Foo=1 Bar=2`. + * @see https://v2.xivapi.com/api/docs#model/querystring + */ +export type QueryString = + | string + | string[] + | Record + | URLSearchParams + | null; + +/** + * Query parameters accepted by endpoints that retrieve excel row data. + * @see https://v2.xivapi.com/api/docs#model/rowreaderquery + */ +export interface RowReaderQuery { + /** + * A filter string for selecting fields within a row. + * Filters are comprised of a comma-seperated list of field paths, i.e. `a,b` will select the fields `a` and `b`. + */ + fields?: FilterString | null; + /** + * Known languages supported by the game data format. **NOTE:** Not all languages that are supported by the format are valid for all editions of the game. For example, the global game client acknowledges the existence of `chs` and `kr`, however does not provide any data for them. + */ + language?: string | SchemaLanguage | null; + /** + * Schema that row data should be read with. + */ + schema?: SchemaSpecifier | null; + /** + * A filter string for selecting fields within a row. + * Filters are comprised of a comma-seperated list of field paths, i.e. `a,b` will select the fields `a` and `b`. + */ + transient?: FilterString | null; +} + +/** + * Known languages supported by the game data format. **NOTE:** Not all languages that are supported by the format are valid for all editions of the game. For example, the global game client acknowledges the existence of `chs` and `kr`, however does not provide any data for them. + * @see https://v2.xivapi.com/api/docs#model/schemalanguage + */ +export enum SchemaLanguage { + none = "none", + en = "en", + ja = "ja", + de = "de", + fr = "fr", + chs = "chs", + cht = "cht", + kr = "kr", +} + +/** + * @see https://v2.xivapi.com/api/docs#model/schemaspecifier + */ +export type SchemaSpecifier = string; // `^.+(@.+)?$` + +/** + * A filter string for selecting fields within a row. + * Filters are comprised of a comma-seperated list of field paths, i.e. `a,b` will select the fields `a` and `b`. + * @see https://v2.xivapi.com/api/docs#model/filterstring + */ +export type FilterString = string | string[]; + +/** + * Response structure for the search endpoint. + * @see https://v2.xivapi.com/api/docs#model/searchresponse + */ +export interface SearchResponse { + results: SearchResult[]; + schema: SchemaSpecifier; + next?: string | null; +} + +/** + * Result found by a search query, hydrated with data from the underlying excel row the result represents. + * @see https://v2.xivapi.com/api/docs#model/searchresult + */ +export interface SearchResult { + fields: Record; + /** + * ID of this row. + */ + row_id: number; + /** + * Relevance score for this entry. + * These values only loosely represent the relevance of an entry to the search query. No guarantee is given that the discrete values, nor resulting sort order, will remain stable. + */ + score: number; + /** + * Excel sheet this result was found in. + */ + sheet: SchemaSpecifier; + /** + * Subrow ID of this row, when relevant. + */ + subrow_id?: number; + /** + * Field values for this row's transient row, if any is present, according to the current schema and transient filter. + */ + transient?: object; +} + +/** + * Response structure for the list endpoint. + * @see https://v2.xivapi.com/api/docs#model/listresponse + */ +export interface ListResponse { + /** + * Array of sheets known to the API. + * Metadata about a single sheet. + */ + sheets: SheetMetadata[]; +} + +/** + * Metadata about a single sheet. + * @see https://v2.xivapi.com/api/docs#model/sheetmetadata + */ +export interface SheetMetadata { + /** + * The name of the sheet. + */ + name: string; +} + +/** + * Path variables accepted by the sheet endpoint. + * @see https://v2.xivapi.com/api/docs#model/sheetpath + */ +export interface SheetPath { + /** + * Name of the sheet to read. + */ + sheet: SchemaSpecifier; +} + +/** + * Query parameters accepted by the sheet endpoint. + * @see https://v2.xivapi.com/api/docs#model/sheetquery + */ +export interface SheetQuery { + /** + * Fetch rows after the specified row. Behavior is undefined if both `rows` and `after` are provided. + */ + after?: SchemaSpecifier | null; + /** + * Maximum number of rows to return. To paginate, provide the last returned row to the next request's `after` parameter. + */ + limit?: number | null; + /** + * Rows to fetch from the sheet, as a comma-separated list. Behavior is undefined if both `rows` and `after` are provided. + */ + rows?: string; // `^\d+(:\d+)?(,\d+(:\d+)?)*$` +} + +/** + * @see https://v2.xivapi.com/api/docs#model/rowspecifier + */ +export type RowSpecifier = string; // `^\d+(:\d+)?$` + +/** + * Response structure for the sheet endpoint. + * @see https://v2.xivapi.com/api/docs#model/sheetresponse + */ +export interface SheetResponse { + /** + * Array of rows retrieved by the query. + * @see https://v2.xivapi.com/api/docs#model/rowresult + */ + rows: RowResult[]; + /** + * The canonical specifier for the schema used in this response. + */ + schema: SchemaSpecifier; +} + +/** + * Row retrieved by the query. + * @see https://v2.xivapi.com/api/docs#model/rowresult + */ +export interface RowResult { + fields: Record; + /** + * ID of this row. + */ + row_id: number; + /** + * Subrow ID of this row, when relevant. + */ + subrow_id?: number | null; + /** + * Field values for this row's transient row, if any is present, according to the current schema and transient filter. + */ + transient?: object; +} + +/** + * Path variables accepted by the row endpoint. + * @see https://v2.xivapi.com/api/docs#model/rowpath + */ +export interface RowPath { + row: RowSpecifier; + /** + * Name of the sheet to read. + */ + sheet: SchemaSpecifier; +} + +/** + * Response structure for the row endpoint. + * @see https://v2.xivapi.com/api/docs#model/rowresponse + */ +export interface RowResponse { + fields: Record; + /** + * ID of this row. + */ + row_id: number; + /** + * The canonical specifier for the schema used in this response. + */ + schema: SchemaSpecifier; + /** + * Subrow ID of this row, when relevant. + */ + subrow_id?: number | null; + /** + * Field values for this row's transient row, if any is present, according to the current schema and transient filter. + */ + transient?: object; +} + +/** + * Response structure for the versions endpoint. + * @see https://v2.xivapi.com/api/docs#model/versionsresponse + */ +export interface VersionsResponse { + /** + * Array of versions available in the API. + * Metadata about a single version supported by the API. + */ + versions: VersionMetadata[]; +} + +/** + * Metadata about a single version supported by the API. + * @see https://v2.xivapi.com/api/docs#model/versionmetadata + */ +export interface VersionMetadata { + /** + * Names associated with this version. Version names specified here are accepted by the `version` query parameter throughout the API. + */ + names: string[]; +} diff --git a/src/utils.ts b/src/utils.ts index d96babe..dcf777b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ -import type { Models, XIVAPI } from "./index.js"; +import type { XIVAPIOptions } from "./index.js"; +import * as Models from "./models.js"; export const endpoint = "https://v2.xivapi.com/api/"; @@ -16,9 +17,10 @@ export interface RequestPayload { data?: unknown; params?: Record; errors?: Models.ErrorResponse[]; - options?: XIVAPI.Options; + options?: XIVAPIOptions; } +/* v8 ignore start -- @preserve */ export const request = async ( payload: RequestPayload ): Promise => { @@ -86,3 +88,4 @@ export const request = async ( return payload; }; +/* v8 ignore stop -- @preserve */ \ No newline at end of file diff --git a/tests/lib/assets.test.ts b/tests/lib/assets.test.ts new file mode 100644 index 0000000..2c0888b --- /dev/null +++ b/tests/lib/assets.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import XIVAPI from "../../src"; + +describe("@xivapi/js", () => { + const xiv = new XIVAPI(); + + describe("data.assets", () => { + const assets = xiv.data.assets(); + + it("should fetch an asset successfully", async () => { + const result = await assets.get({ + path: "ui/icon/051000/051474_hr1.tex", + format: "png", + }); + + expect(result).toBeDefined(); + expect(Buffer.isBuffer(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + }); + + it("should handle map requests correctly", async () => { + const result = await assets.map("s1d1", "00", { version: "latest" }); + + expect(result).toBeDefined(); + expect(Buffer.isBuffer(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + }); + + it("should throw CustomError when asset fetch fails", async () => { + await expect( + assets.get({ + path: "invalid/path/that/does/not/exist.tex", + format: "png", + }) + ).rejects.toThrow(); + }); + }); +}); diff --git a/tests/lib/sheets.test.ts b/tests/lib/sheets.test.ts new file mode 100644 index 0000000..7202eca --- /dev/null +++ b/tests/lib/sheets.test.ts @@ -0,0 +1,148 @@ +import { describe, it, expect } from "vitest"; +import XIVAPI from "../../src"; + +describe("@xivapi/js", () => { + const xiv = new XIVAPI(); + const custom = new XIVAPI({ language: "fr", verbose: true }); + + describe("data.sheets", () => { + const sheets = xiv.data.sheets(); + + describe("error handling", () => { + it("throws error for non-existent sheets", async () => { + await expect( + sheets.list("NonExistentSheetThatDoesNotExist") + ).rejects.toThrow(); + await expect( + sheets.get("NonExistentSheetThatDoesNotExist", 1) + ).rejects.toThrow(); + }); + }); + + describe("data.sheets.all", () => { + it("can list all available sheets", async () => { + const result = await sheets.all(); + + expect(result).toBeDefined(); + expect(result.sheets).toBeDefined(); + expect(Array.isArray(result.sheets)).toBe(true); + expect(result.sheets.length).toBeGreaterThan(0); + + result.sheets.forEach((sheet) => { + expect(sheet.name).toBeDefined(); + expect(typeof sheet.name).toBe("string"); + }); + }); + + it("can list all sheets with custom options", async () => { + const sheets = custom.data.sheets(); + const result = await sheets.all(); + + expect(result).toBeDefined(); + expect(result.sheets).toBeDefined(); + expect(Array.isArray(result.sheets)).toBe(true); + expect(result.sheets.length).toBeGreaterThan(0); + }); + }); + + describe("data.sheets.list", () => { + it("can list rows with default parameters", async () => { + const result = await sheets.list("Item"); + + expect(result).toBeDefined(); + expect(result.rows).toBeDefined(); + expect(Array.isArray(result.rows)).toBe(true); + expect(result.schema).toBeDefined(); + expect(result.rows.length).toBeGreaterThan(0); + }); + + it("can list rows from a specific sheet", async () => { + const result = await sheets.list("Item", { limit: 5 }); + + expect(result).toBeDefined(); + expect(result.rows).toBeDefined(); + expect(Array.isArray(result.rows)).toBe(true); + expect(result.schema).toBeDefined(); + expect(result.rows.length).toBeGreaterThan(0); + + result.rows.forEach((row) => { + expect(row.row_id).toBeDefined(); + expect(typeof row.row_id).toBe("number"); + expect(row.fields).toBeDefined(); + expect(typeof row.fields).toBe("object"); + + if (row.fields.Name) { + expect(typeof row.fields.Name).toBe("string"); + expect((row.fields.Name as string).length).toBeGreaterThan(0); + } + + if (row.fields.ID) { + expect(typeof row.fields.ID).toBe("number"); + expect(row.fields.ID).toBeGreaterThan(0); + } + }); + }); + + it("can list rows with custom options", async () => { + const sheets = custom.data.sheets(); + const result = await sheets.list("Item", { limit: 5 }); + + expect(result).toBeDefined(); + expect(result.rows).toBeDefined(); + expect(Array.isArray(result.rows)).toBe(true); + expect(result.schema).toBeDefined(); + expect(result.rows.length).toBeGreaterThan(0); + }); + }); + + describe("data.sheets.get", () => { + it("can get a specific row", async () => { + const result = await sheets.get("Item", "1", { fields: "Name" }); + + expect(result).toBeDefined(); + expect(result.row_id).toBeDefined(); + expect(typeof result.row_id).toBe("number"); + expect(result.schema).toBeDefined(); + expect(result.fields).toBeDefined(); + expect(result.row_id).toBe(1); + + expect((result.fields as any).Name).toBeDefined(); + expect((result.fields as any).Name).toBe("Gil"); + }); + + it("can get a specific row with array-based field filtering", async () => { + const result = await sheets.get("Item", "1", { + fields: ["Name", "LevelItem"], + }); + + expect(result).toBeDefined(); + expect(result.row_id).toBeDefined(); + expect(typeof result.row_id).toBe("number"); + expect(result.schema).toBeDefined(); + expect(result.fields).toBeDefined(); + expect(result.row_id).toBe(1); + + expect(result.fields).toBeDefined(); + expect(typeof result.fields).toBe("object"); + expect(Object.keys(result.fields).length).toBe(2); + expect((result.fields as any).Name).toBeDefined(); + expect((result.fields as any).Name).toBe("Gil"); + }); + + it("can get a specific row with custom options", async () => { + const sheets = custom.data.sheets(); + const result = await sheets.get("Item", "1", { fields: "Name" }); + + expect(result).toBeDefined(); + expect(result.row_id).toBeDefined(); + expect(typeof result.row_id).toBe("number"); + expect(result.schema).toBeDefined(); + expect(result.fields).toBeDefined(); + expect(result.row_id).toBe(1); + + expect((result.fields as any).Name).toBeDefined(); + expect((result.fields as any).Name).toBe("Gil"); + }); + }); + }); +}); diff --git a/tests/lib/versions.test.ts b/tests/lib/versions.test.ts new file mode 100644 index 0000000..9465f5f --- /dev/null +++ b/tests/lib/versions.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from "vitest"; +import XIVAPI from "../../src"; + +describe("@xivapi/js", () => { + const xiv = new XIVAPI(); + + describe("data.versions", () => { + it("should fetch all versions successfully", async () => { + const result = await xiv.data.versions(); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + + result.forEach((version) => { + expect(typeof version).toBe("string"); + expect(version.length).toBeGreaterThan(0); + }); + }); + }); +}); diff --git a/tests/lib/xivapi.test.ts b/tests/lib/xivapi.test.ts new file mode 100644 index 0000000..43455ee --- /dev/null +++ b/tests/lib/xivapi.test.ts @@ -0,0 +1,518 @@ +import { describe, it, expect } from "vitest"; +import { XIVAPI, Models } from "../../src"; + +describe("@xivapi/js", () => { + const xiv = new XIVAPI(); + + describe("client options", () => { + it("can create client with custom options", async () => { + const custom = new XIVAPI({ + language: "ja" as const, + verbose: true, + version: "latest", + }); + + const result = await custom.items.get(1, { + fields: "Name", + }); + + expect(result).toBeDefined(); + expect(result.row_id).toBe(1); + expect((result.fields as any).Name).toBe("ギル"); + }); + }); + + const custom = new XIVAPI({ language: "fr", verbose: true }); + + describe("search", () => { + const validateResponse = (res: Models.SearchResponse) => { + expect(res).toBeDefined(); + expect(res.results).toBeDefined(); + expect(Array.isArray(res.results)).toBe(true); + expect(res.schema).toBeDefined(); + + if (res.results.length > 0) { + const firstResult = res.results[0]; + expect(firstResult.row_id).toBeDefined(); + expect(typeof firstResult.row_id).toBe("number"); + expect(firstResult.score).toBeDefined(); + expect(typeof firstResult.score).toBe("number"); + expect(firstResult.sheet).toBeDefined(); + expect(firstResult.fields).toBeDefined(); + } + }; + + const validateItemData = (res: Models.SearchResponse) => { + expect(res.results.length).toBeGreaterThan(0); + + res.results.forEach((item) => { + expect(item.sheet).toBe("Item"); + expect(item.fields).toBeDefined(); + expect(typeof item.fields).toBe("object"); + + if (item.fields.Name) { + expect(typeof item.fields.Name).toBe("string"); + expect((item.fields.Name as string).length).toBeGreaterThan(0); + } + + if (item.fields.ID) { + expect(typeof item.fields.ID).toBe("number"); + expect(item.fields.ID).toBeGreaterThan(0); + } + + if (item.fields.LevelItem) { + expect(typeof item.fields.LevelItem).toBe("number"); + expect(item.fields.LevelItem).toBeGreaterThanOrEqual(0); + } + }); + }; + + const validateActionData = (res: Models.SearchResponse) => { + expect(res.results.length).toBeGreaterThan(0); + + res.results.forEach((action) => { + expect(action.sheet).toBe("Action"); + expect(action.fields).toBeDefined(); + expect(typeof action.fields).toBe("object"); + + if (action.fields.Name) { + expect(typeof action.fields.Name).toBe("string"); + expect((action.fields.Name as string).length).toBeGreaterThan(0); + } + + if (action.fields.ID) { + expect(typeof action.fields.ID).toBe("number"); + expect(action.fields.ID).toBeGreaterThan(0); + } + }); + }; + + describe("error handling", () => { + it("throws error for invalid query syntax", async () => { + await expect( + xiv.search({ + query: "invalid query syntax that should fail", + sheets: "Item", + }) + ).rejects.toThrow(); + }); + }); + + describe("basic search operations", () => { + it("can find items by via an exact name match", async () => { + const result = await xiv.search({ + query: 'Name="Iron War Axe"', + sheets: "Item", + limit: 5, + }); + + validateResponse(result); + validateItemData(result); + + const ironWarAxe = result.results.find( + (item) => item.fields.Name === "Iron War Axe" + ); + + expect(ironWarAxe).toBeDefined(); + expect(ironWarAxe!.fields.Name).toBe("Iron War Axe"); + }); + + it("can find items using partial text search", async () => { + const result = await xiv.search({ + query: 'Name~"sword"', + sheets: "Item", + limit: 5, + }); + + validateResponse(result); + validateItemData(result); + + result.results.forEach((item) => { + expect((item.fields.Name as string).toLowerCase()).toContain("sword"); + }); + }); + + it("can search actions by numeric properties", async () => { + const result = await xiv.search({ + query: "Recast100ms>3000", + sheets: "Action", + limit: 5, + }); + + validateResponse(result); + validateActionData(result); + + result.results.forEach((action) => { + if (action.fields.Recast100ms) { + expect(action.fields.Recast100ms).toBeGreaterThan(3000); + } + }); + }); + }); + + describe("numeric comparisons", () => { + it("can find high-level items (>=50)", async () => { + const result = await xiv.search({ + query: "LevelItem>=50", + sheets: "Item", + limit: 5, + }); + + validateResponse(result); + validateItemData(result); + + result.results.forEach((item) => { + if (item.fields.LevelItem) { + expect(item.fields.LevelItem).toBeGreaterThanOrEqual(50); + } + }); + }); + + it("can find low-level items (<10)", async () => { + const result = await xiv.search({ + query: "LevelItem<10", + sheets: "Item", + limit: 5, + }); + + validateResponse(result); + validateItemData(result); + + result.results.forEach((item) => { + if (item.fields.LevelItem) { + expect(item.fields.LevelItem).toBeLessThan(10); + } + }); + }); + + it("can find items in a level range", async () => { + const result = await xiv.search({ + query: "LevelItem>=90 LevelItem<=99", + sheets: "Item", + limit: 5, + }); + + validateResponse(result); + validateItemData(result); + + result.results.forEach((item) => { + if (item.fields.LevelItem) { + expect(item.fields.LevelItem).toBeGreaterThanOrEqual(90); + expect(item.fields.LevelItem).toBeLessThanOrEqual(99); + } + }); + }); + }); + + describe("boolean and complex queries", () => { + it("can filter by boolean properties", async () => { + const result = await xiv.search({ + query: "IsUntradable=true", + sheets: "Item", + limit: 5, + }); + + validateResponse(result); + validateItemData(result); + + result.results.forEach((item) => { + if (item.fields.IsUntradable !== undefined) { + expect(item.fields.IsUntradable).toBe(true); + } + }); + }); + + it("can combine multiple search criteria", async () => { + const result = await xiv.search({ + query: ['Name~"sword"', "LevelItem>=10", "LevelItem<=50"], + sheets: "Item", + limit: 5, + }); + + validateResponse(result); + validateItemData(result); + + result.results.forEach((item) => { + if (item.fields.Name) { + expect((item.fields.Name as string).toLowerCase()).toContain( + "sword" + ); + } + if (item.fields.LevelItem) { + expect(item.fields.LevelItem).toBeGreaterThanOrEqual(10); + expect(item.fields.LevelItem).toBeLessThanOrEqual(50); + } + }); + }); + }); + + describe("multi-sheet searches", () => { + it("can search across multiple sheets simultaneously", async () => { + const result = await xiv.search({ + query: 'Name~"rainbow"', + sheets: "Action,Item", + limit: 5, + }); + + validateResponse(result); + + if (result.results.length > 0) { + const sheets = new Set(result.results.map((r) => r.sheet)); + expect(sheets.size).toBeGreaterThanOrEqual(1); + } + }); + }); + + describe("pagination", () => { + it("can limit results and get pagination cursor", async () => { + const result = await xiv.search({ + query: 'Name~"rainbow"', + sheets: "Item", + limit: 2, + }); + + expect(result).toBeDefined(); + expect(result.results).toBeDefined(); + expect(Array.isArray(result.results)).toBe(true); + expect(result.results.length).toBeLessThanOrEqual(2); + expect(result.schema).toBeDefined(); + + if (result.next) { + expect(typeof result.next).toBe("string"); + expect(result.next.length).toBeGreaterThan(0); + } + }); + + it("can paginate through results using cursor", async () => { + const firstPage = await xiv.search({ + query: 'Name~"sword"', + sheets: "Item", + limit: 2, + }); + + expect(firstPage).toBeDefined(); + expect(firstPage.results).toBeDefined(); + expect(Array.isArray(firstPage.results)).toBe(true); + + if (firstPage.next) { + const secondPage = await xiv.search({ + cursor: firstPage.next, + limit: 2, + }); + + expect(secondPage).toBeDefined(); + expect(secondPage.results).toBeDefined(); + expect(Array.isArray(secondPage.results)).toBe(true); + expect(secondPage.schema).toBeDefined(); + } + }); + }); + + describe("customization", () => { + it("can use custom language and verbose options", async () => { + const result = await custom.search({ + query: 'Name~"sword"', + sheets: "Item", + limit: 3, + }); + + validateResponse(result); + + expect(result.results.length).toBeLessThanOrEqual(3); + expect(result.schema).toBeDefined(); + }); + }); + + describe("array-based queries", () => { + it("can use string arrays for complex queries", async () => { + const result = await xiv.search({ + query: ['Name~"sword"', "LevelItem>=10", "LevelItem<=50"], + sheets: "Item", + limit: 5, + }); + + validateResponse(result); + validateItemData(result); + + result.results.forEach((item) => { + if (item.fields.Name) { + expect((item.fields.Name as string).toLowerCase()).toContain( + "sword" + ); + } + if (item.fields.LevelItem) { + expect(item.fields.LevelItem).toBeGreaterThanOrEqual(10); + expect(item.fields.LevelItem).toBeLessThanOrEqual(50); + } + }); + }); + + it("can use string arrays for simple queries", async () => { + const result = await xiv.search({ + query: ['Name~"sword"'], + sheets: "Item", + limit: 5, + }); + + validateResponse(result); + validateItemData(result); + + result.results.forEach((item) => { + if (item.fields.Name) { + expect((item.fields.Name as string).toLowerCase()).toContain( + "sword" + ); + } + }); + }); + }); + }); + + describe("typed sheet accessors", () => { + describe("items", () => { + it("can get a specific item by ID", async () => { + const result = await xiv.items.get(1, { + fields: "Name", + language: "en", + }); + + expect(result).toBeDefined(); + expect(result.row_id).toBeDefined(); + expect(typeof result.row_id).toBe("number"); + expect(result.schema).toBeDefined(); + expect(result.fields).toBeDefined(); + expect(result.row_id).toBe(1); + expect((result.fields as any).Name).toBe("Gil"); + }); + + it("can list items with parameters", async () => { + const result = await xiv.items.list({ limit: 3 }); + + expect(result).toBeDefined(); + expect(result.rows).toBeDefined(); + expect(Array.isArray(result.rows)).toBe(true); + expect(result.schema).toBeDefined(); + expect(result.rows.length).toBeLessThanOrEqual(3); + + result.rows.forEach((row: any) => { + expect(row.row_id).toBeDefined(); + expect(typeof row.row_id).toBe("number"); + expect(row.fields).toBeDefined(); + expect(typeof row.fields).toBe("object"); + }); + }); + }); + + describe("achievements", () => { + it("can get a specific achievement by ID", async () => { + const result = await xiv.achievements.get(1, { + fields: "Name", + language: "en", + }); + + expect(result).toBeDefined(); + expect(result.row_id).toBeDefined(); + expect(typeof result.row_id).toBe("number"); + expect(result.schema).toBeDefined(); + expect(result.fields).toBeDefined(); + expect(result.row_id).toBe(1); + expect((result.fields as any).Name).toBe("To Crush Your Enemies I"); + }); + + it("can list achievements with parameters", async () => { + const result = await xiv.achievements.list({ limit: 3 }); + + expect(result).toBeDefined(); + expect(result.rows).toBeDefined(); + expect(Array.isArray(result.rows)).toBe(true); + expect(result.schema).toBeDefined(); + expect(result.rows.length).toBeLessThanOrEqual(3); + + result.rows.forEach((row: any) => { + expect(row.row_id).toBeDefined(); + expect(typeof row.row_id).toBe("number"); + expect(row.fields).toBeDefined(); + expect(typeof row.fields).toBe("object"); + }); + }); + }); + + describe("minions", () => { + it("can get a specific minion by ID", async () => { + const result = await xiv.minions.get(1, { + fields: "Singular", + language: "en", + }); + + expect(result).toBeDefined(); + expect(result.row_id).toBeDefined(); + expect(typeof result.row_id).toBe("number"); + expect(result.schema).toBeDefined(); + expect(result.fields).toBeDefined(); + expect(result.row_id).toBe(1); + expect((result.fields as any).Singular).toBe("cherry bomb"); + }); + + it("can list minions with parameters", async () => { + const result = await xiv.minions.list({ limit: 3 }); + + expect(result).toBeDefined(); + expect(result.rows).toBeDefined(); + expect(Array.isArray(result.rows)).toBe(true); + expect(result.schema).toBeDefined(); + expect(result.rows.length).toBeLessThanOrEqual(3); + + result.rows.forEach((row: any) => { + expect(row.row_id).toBeDefined(); + expect(typeof row.row_id).toBe("number"); + expect(row.fields).toBeDefined(); + expect(typeof row.fields).toBe("object"); + + if (row.fields.Singular) { + expect(typeof row.fields.Singular).toBe("string"); + expect(row.fields.Singular.length).toBeGreaterThan(0); + } + }); + }); + }); + + describe("mounts", () => { + it("can get a specific mount by ID", async () => { + const result = await xiv.mounts.get(1, { + fields: "Singular", + language: "en", + }); + + expect(result).toBeDefined(); + expect(result.row_id).toBeDefined(); + expect(typeof result.row_id).toBe("number"); + expect(result.schema).toBeDefined(); + expect(result.fields).toBeDefined(); + expect(result.row_id).toBe(1); + expect((result.fields as any).Singular).toBe("company chocobo"); + }); + + it("can list mounts with parameters", async () => { + const result = await xiv.mounts.list({ limit: 3 }); + + expect(result).toBeDefined(); + expect(result.rows).toBeDefined(); + expect(Array.isArray(result.rows)).toBe(true); + expect(result.schema).toBeDefined(); + expect(result.rows.length).toBeLessThanOrEqual(3); + + result.rows.forEach((row: any) => { + expect(row.row_id).toBeDefined(); + expect(typeof row.row_id).toBe("number"); + expect(row.fields).toBeDefined(); + expect(typeof row.fields).toBe("object"); + + if (row.fields.Name) { + expect(typeof row.fields.Name).toBe("string"); + expect(row.fields.Name.length).toBeGreaterThan(0); + } + }); + }); + }); + }); +}); diff --git a/tests/xivapi-js.test.ts b/tests/xivapi-js.test.ts deleted file mode 100644 index 90a9fc1..0000000 --- a/tests/xivapi-js.test.ts +++ /dev/null @@ -1,825 +0,0 @@ -import { describe, it, expect } from "vitest"; -import xivapiClient from "../src/index"; - -describe("@xivapi/js", () => { - const API_TIMEOUT = 10000; - const xivapi = new xivapiClient(); - - const validateSearchResponse = (result: any) => { - expect(result).toBeDefined(); - expect(result.results).toBeDefined(); - expect(Array.isArray(result.results)).toBe(true); - expect(result.schema).toBeDefined(); - - if (result.results.length > 0) { - const firstResult = result.results[0]; - expect(firstResult.row_id).toBeDefined(); - expect(typeof firstResult.row_id).toBe("number"); - expect(firstResult.score).toBeDefined(); - expect(typeof firstResult.score).toBe("number"); - expect(firstResult.sheet).toBeDefined(); - expect(firstResult.fields).toBeDefined(); - } - }; - - const validateItemData = (result: any, expectedSheet: string = "Item") => { - expect(result.results.length).toBeGreaterThan(0); - - result.results.forEach((item: any) => { - expect(item.sheet).toBe(expectedSheet); - expect(item.fields).toBeDefined(); - expect(typeof item.fields).toBe("object"); - - if (item.fields.Name) { - expect(typeof item.fields.Name).toBe("string"); - expect(item.fields.Name.length).toBeGreaterThan(0); - } - - if (item.fields.ID) { - expect(typeof item.fields.ID).toBe("number"); - expect(item.fields.ID).toBeGreaterThan(0); - } - - if (item.fields.LevelItem) { - expect(typeof item.fields.LevelItem).toBe("number"); - expect(item.fields.LevelItem).toBeGreaterThanOrEqual(0); - } - }); - }; - - const validateActionData = (result: any) => { - expect(result.results.length).toBeGreaterThan(0); - - result.results.forEach((action: any) => { - expect(action.sheet).toBe("Action"); - expect(action.fields).toBeDefined(); - expect(typeof action.fields).toBe("object"); - - if (action.fields.Name) { - expect(typeof action.fields.Name).toBe("string"); - expect(action.fields.Name.length).toBeGreaterThan(0); - } - - if (action.fields.ID) { - expect(typeof action.fields.ID).toBe("number"); - expect(action.fields.ID).toBeGreaterThan(0); - } - }); - }; - - describe("versions", () => { - it( - "should fetch all versions successfully", - async () => { - const result = await xivapi.data.versions(); - - expect(result).toBeDefined(); - expect(Array.isArray(result)).toBe(true); - expect(result.length).toBeGreaterThan(0); - - result.forEach((version) => { - expect(typeof version).toBe("string"); - expect(version.length).toBeGreaterThan(0); - }); - }, - API_TIMEOUT - ); - }); - - describe("assets", () => { - it( - "should fetch asset successfully", - async () => { - const assets = xivapi.data.assets(); - const result = await assets.get({ - path: "ui/icon/051000/051474_hr1.tex", - format: "png", - }); - - expect(result).toBeDefined(); - expect(Buffer.isBuffer(result)).toBe(true); - expect(result.length).toBeGreaterThan(0); - }, - API_TIMEOUT - ); - - it( - "should handle map requests correctly", - async () => { - const assets = xivapi.data.assets(); - const result = await assets.map("s1d1", "00", { version: "latest" }) - - expect(result).toBeDefined(); - expect(Buffer.isBuffer(result)).toBe(true); - expect(result.length).toBeGreaterThan(0); - }, - API_TIMEOUT - ); - - it( - "should throw CustomError when asset fetch fails", - async () => { - const assets = xivapi.data.assets(); - - await expect( - assets.get({ - path: "invalid/path/that/does/not/exist.tex", - format: "png", - }) - ).rejects.toThrow(); - }, - API_TIMEOUT - ); - }); - - describe("search", () => { - describe("basic search operations", () => { - it( - "can find items by exact name match", - async () => { - const result = await xivapi.search({ - query: 'Name="Iron War Axe"', - sheets: "Item", - limit: 5, - }); - - validateSearchResponse(result); - validateItemData(result); - - const ironWarAxe = result.results.find( - (item: any) => item.fields.Name === "Iron War Axe" - ); - expect(ironWarAxe).toBeDefined(); - if (ironWarAxe) { - expect((ironWarAxe.fields as any).Name).toBe("Iron War Axe"); - } - }, - API_TIMEOUT - ); - - it( - "can find items using partial text search", - async () => { - const result = await xivapi.search({ - query: 'Name~"sword"', - sheets: "Item", - limit: 5, - }); - - validateSearchResponse(result); - validateItemData(result); - - result.results.forEach((item: any) => { - if (item.fields.Name) { - expect(item.fields.Name.toLowerCase()).toContain("sword"); - } - }); - }, - API_TIMEOUT - ); - - it( - "can search actions by numeric properties", - async () => { - const result = await xivapi.search({ - query: "Recast100ms>3000", - sheets: "Action", - limit: 5, - }); - - validateSearchResponse(result); - validateActionData(result); - - result.results.forEach((action: any) => { - if (action.fields.Recast100ms) { - expect(action.fields.Recast100ms).toBeGreaterThan(3000); - } - }); - }, - API_TIMEOUT - ); - }); - - describe("numeric comparisons", () => { - it( - "can find high-level items (>=50)", - async () => { - const result = await xivapi.search({ - query: "LevelItem>=50", - sheets: "Item", - limit: 5, - }); - - validateSearchResponse(result); - validateItemData(result); - - result.results.forEach((item: any) => { - if (item.fields.LevelItem) { - expect(item.fields.LevelItem).toBeGreaterThanOrEqual(50); - } - }); - }, - API_TIMEOUT - ); - - it( - "can find low-level items (<10)", - async () => { - const result = await xivapi.search({ - query: "LevelItem<10", - sheets: "Item", - limit: 5, - }); - - validateSearchResponse(result); - validateItemData(result); - - result.results.forEach((item: any) => { - if (item.fields.LevelItem) { - expect(item.fields.LevelItem).toBeLessThan(10); - } - }); - }, - API_TIMEOUT - ); - - it( - "can find items in a level range", - async () => { - const result = await xivapi.search({ - query: "LevelItem>=90 LevelItem<=99", - sheets: "Item", - limit: 5, - }); - - validateSearchResponse(result); - validateItemData(result); - - result.results.forEach((item: any) => { - if (item.fields.LevelItem) { - expect(item.fields.LevelItem).toBeGreaterThanOrEqual(90); - expect(item.fields.LevelItem).toBeLessThanOrEqual(99); - } - }); - }, - API_TIMEOUT - ); - }); - - describe("boolean and complex queries", () => { - it( - "can filter by boolean properties", - async () => { - const result = await xivapi.search({ - query: "IsUntradable=true", - sheets: "Item", - limit: 5, - }); - - validateSearchResponse(result); - validateItemData(result); - - result.results.forEach((item: any) => { - if (item.fields.IsUntradable !== undefined) { - expect(item.fields.IsUntradable).toBe(true); - } - }); - }, - API_TIMEOUT - ); - - it( - "can combine multiple search criteria", - async () => { - const result = await xivapi.search({ - query: 'Name~"sword" LevelItem>=10 LevelItem<=50', - sheets: "Item", - limit: 5, - }); - - validateSearchResponse(result); - validateItemData(result); - - result.results.forEach((item: any) => { - if (item.fields.Name) { - expect(item.fields.Name.toLowerCase()).toContain("sword"); - } - if (item.fields.LevelItem) { - expect(item.fields.LevelItem).toBeGreaterThanOrEqual(10); - expect(item.fields.LevelItem).toBeLessThanOrEqual(50); - } - }); - }, - API_TIMEOUT - ); - }); - - describe("multi-sheet searches", () => { - it( - "can search across multiple sheets simultaneously", - async () => { - const result = await xivapi.search({ - query: 'Name~"rainbow"', - sheets: "Action,Item", - limit: 5, - }); - - validateSearchResponse(result); - - if (result.results.length > 0) { - const sheets = new Set(result.results.map((r) => r.sheet)); - expect(sheets.size).toBeGreaterThanOrEqual(1); - } - }, - API_TIMEOUT - ); - }); - - describe("pagination", () => { - it( - "can limit results and get pagination cursor", - async () => { - const result = await xivapi.search({ - query: 'Name~"rainbow"', - sheets: "Item", - limit: 2, - }); - - expect(result).toBeDefined(); - expect(result.results).toBeDefined(); - expect(Array.isArray(result.results)).toBe(true); - expect(result.results.length).toBeLessThanOrEqual(2); - expect(result.schema).toBeDefined(); - - if (result.next) { - expect(typeof result.next).toBe("string"); - expect(result.next.length).toBeGreaterThan(0); - } - }, - API_TIMEOUT - ); - - it( - "can paginate through results using cursor", - async () => { - const firstPage = await xivapi.search({ - query: 'Name~"sword"', - sheets: "Item", - limit: 2, - }); - - expect(firstPage).toBeDefined(); - expect(firstPage.results).toBeDefined(); - expect(Array.isArray(firstPage.results)).toBe(true); - - if (firstPage.next) { - const secondPage = await xivapi.search({ - cursor: firstPage.next, - limit: 2, - }); - - expect(secondPage).toBeDefined(); - expect(secondPage.results).toBeDefined(); - expect(Array.isArray(secondPage.results)).toBe(true); - expect(secondPage.schema).toBeDefined(); - } - }, - API_TIMEOUT - ); - }); - - describe("customization", () => { - it( - "can use custom language and verbose options", - async () => { - const result = await xivapi.search({ - query: 'Name~"sword"', - sheets: "Item", - limit: 3, - }); - - validateSearchResponse(result); - - expect(result.results.length).toBeLessThanOrEqual(3); - expect(result.schema).toBeDefined(); - }, - API_TIMEOUT - ); - }); - - describe("array-based queries", () => { - it( - "can use string arrays for complex queries", - async () => { - const result = await xivapi.search({ - query: ['Name~"sword"', "LevelItem>=10", "LevelItem<=50"], - sheets: "Item", - limit: 5, - }); - - validateSearchResponse(result); - validateItemData(result); - - result.results.forEach((item: any) => { - if (item.fields.Name) { - expect(item.fields.Name.toLowerCase()).toContain("sword"); - } - if (item.fields.LevelItem) { - expect(item.fields.LevelItem).toBeGreaterThanOrEqual(10); - expect(item.fields.LevelItem).toBeLessThanOrEqual(50); - } - }); - }, - API_TIMEOUT - ); - - it( - "can use string arrays for simple queries", - async () => { - const result = await xivapi.search({ - query: ['Name~"sword"'], - sheets: "Item", - limit: 5, - }); - - validateSearchResponse(result); - validateItemData(result); - - result.results.forEach((item: any) => { - if (item.fields.Name) { - expect(item.fields.Name.toLowerCase()).toContain("sword"); - } - }); - }, - API_TIMEOUT - ); - }); - - describe("error handling", () => { - it( - "throws error for invalid query syntax", - async () => { - await expect( - xivapi.search({ - query: "invalid query syntax that should fail", - sheets: "Item", - }) - ).rejects.toThrow(); - }, - API_TIMEOUT - ); - }); - }); - - describe("sheets", () => { - describe("listing sheets", () => { - it( - "can list all available sheets", - async () => { - const sheets = xivapi.data.sheets(); - const result = await sheets.all(); - - expect(result).toBeDefined(); - expect(result.sheets).toBeDefined(); - expect(Array.isArray(result.sheets)).toBe(true); - expect(result.sheets.length).toBeGreaterThan(0); - - result.sheets.forEach((sheet) => { - expect(sheet.name).toBeDefined(); - expect(typeof sheet.name).toBe("string"); - }); - }, - API_TIMEOUT - ); - - it( - "can list sheets with custom language options", - async () => { - const client = new xivapiClient({ - language: "fr" as const, - verbose: true, - }); - const sheets = client.data.sheets(); - const result = await sheets.all(); - - expect(result).toBeDefined(); - expect(result.sheets).toBeDefined(); - expect(Array.isArray(result.sheets)).toBe(true); - expect(result.sheets.length).toBeGreaterThan(0); - }, - API_TIMEOUT - ); - }); - - describe("reading sheet data", () => { - it( - "can list rows from a specific sheet", - async () => { - const sheets = xivapi.data.sheets(); - const result = await sheets.list("Item", { limit: 5 }); - - expect(result).toBeDefined(); - expect(result.rows).toBeDefined(); - expect(Array.isArray(result.rows)).toBe(true); - expect(result.schema).toBeDefined(); - expect(result.rows.length).toBeGreaterThan(0); - - result.rows.forEach((row: any) => { - expect(row.row_id).toBeDefined(); - expect(typeof row.row_id).toBe("number"); - expect(row.fields).toBeDefined(); - expect(typeof row.fields).toBe("object"); - - if (row.fields.Name) { - expect(typeof row.fields.Name).toBe("string"); - expect(row.fields.Name.length).toBeGreaterThan(0); - } - - if (row.fields.ID) { - expect(typeof row.fields.ID).toBe("number"); - expect(row.fields.ID).toBeGreaterThan(0); - } - }); - }, - API_TIMEOUT - ); - - it( - "can get a specific row with field filtering", - async () => { - const sheets = xivapi.data.sheets(); - const result = await sheets.get("Item", "1", { - fields: "Name", - language: "en", - }); - - expect(result).toBeDefined(); - expect(result.row_id).toBeDefined(); - expect(typeof result.row_id).toBe("number"); - expect(result.schema).toBeDefined(); - expect(result.fields).toBeDefined(); - expect(result.row_id).toBe(1); - - expect((result.fields as any).Name).toBeDefined(); - expect((result.fields as any).Name).toBe("Gil"); - }, - API_TIMEOUT - ); - - it( - "can get a specific row with array-based field filtering", - async () => { - const sheets = xivapi.data.sheets(); - const result = await sheets.get("Item", "1", { - fields: ["Name", "LevelItem"], - language: "en", - }); - - expect(result).toBeDefined(); - expect(result.row_id).toBeDefined(); - expect(typeof result.row_id).toBe("number"); - expect(result.schema).toBeDefined(); - expect(result.fields).toBeDefined(); - expect(result.row_id).toBe(1); - - expect(result.fields).toBeDefined(); - expect(typeof result.fields).toBe("object"); - expect(Object.keys(result.fields).length).toBe(2); - expect((result.fields as any).Name).toBeDefined(); - expect((result.fields as any).Name).toBe("Gil"); - }, - API_TIMEOUT - ); - - it( - "can list rows with default parameters", - async () => { - const sheets = xivapi.data.sheets(); - const result = await sheets.list("Item"); - - expect(result).toBeDefined(); - expect(result.rows).toBeDefined(); - expect(Array.isArray(result.rows)).toBe(true); - expect(result.schema).toBeDefined(); - }, - API_TIMEOUT - ); - }); - - describe("error handling", () => { - it( - "throws error for non-existent sheets", - async () => { - const sheets = xivapi.data.sheets(); - - await expect( - sheets.list("NonExistentSheetThatDoesNotExist") - ).rejects.toThrow(); - }, - API_TIMEOUT - ); - }); - }); - - describe("typed sheet accessors", () => { - describe("items", () => { - it( - "can get a specific item by ID", - async () => { - const result = await xivapi.items.get(1, { - fields: "Name", - language: "en", - }); - - expect(result).toBeDefined(); - expect(result.row_id).toBeDefined(); - expect(typeof result.row_id).toBe("number"); - expect(result.schema).toBeDefined(); - expect(result.fields).toBeDefined(); - expect(result.row_id).toBe(1); - expect((result.fields as any).Name).toBe("Gil"); - }, - API_TIMEOUT - ); - - it( - "can list items with parameters", - async () => { - const result = await xivapi.items.list({ limit: 3 }); - - expect(result).toBeDefined(); - expect(result.rows).toBeDefined(); - expect(Array.isArray(result.rows)).toBe(true); - expect(result.schema).toBeDefined(); - expect(result.rows.length).toBeLessThanOrEqual(3); - - result.rows.forEach((row: any) => { - expect(row.row_id).toBeDefined(); - expect(typeof row.row_id).toBe("number"); - expect(row.fields).toBeDefined(); - expect(typeof row.fields).toBe("object"); - }); - }, - API_TIMEOUT - ); - }); - - describe("achievements", () => { - it( - "can get a specific achievement by ID", - async () => { - const result = await xivapi.achievements.get(1, { - fields: "Name", - language: "en", - }); - - expect(result).toBeDefined(); - expect(result.row_id).toBeDefined(); - expect(typeof result.row_id).toBe("number"); - expect(result.schema).toBeDefined(); - expect(result.fields).toBeDefined(); - expect(result.row_id).toBe(1); - expect((result.fields as any).Name).toBe("To Crush Your Enemies I"); - }, - API_TIMEOUT - ); - - it( - "can list achievements with parameters", - async () => { - const result = await xivapi.achievements.list({ limit: 3 }); - - expect(result).toBeDefined(); - expect(result.rows).toBeDefined(); - expect(Array.isArray(result.rows)).toBe(true); - expect(result.schema).toBeDefined(); - expect(result.rows.length).toBeLessThanOrEqual(3); - - result.rows.forEach((row: any) => { - expect(row.row_id).toBeDefined(); - expect(typeof row.row_id).toBe("number"); - expect(row.fields).toBeDefined(); - expect(typeof row.fields).toBe("object"); - }); - }, - API_TIMEOUT - ); - }); - - describe("minions", () => { - it( - "can get a specific minion by ID", - async () => { - const result = await xivapi.minions.get(1, { - fields: "Singular", - language: "en", - }); - - expect(result).toBeDefined(); - expect(result.row_id).toBeDefined(); - expect(typeof result.row_id).toBe("number"); - expect(result.schema).toBeDefined(); - expect(result.fields).toBeDefined(); - expect(result.row_id).toBe(1); - expect((result.fields as any).Singular).toBe("cherry bomb"); - }, - API_TIMEOUT - ); - - it( - "can list minions with parameters", - async () => { - const result = await xivapi.minions.list({ limit: 3 }); - - expect(result).toBeDefined(); - expect(result.rows).toBeDefined(); - expect(Array.isArray(result.rows)).toBe(true); - expect(result.schema).toBeDefined(); - expect(result.rows.length).toBeLessThanOrEqual(3); - - result.rows.forEach((row: any) => { - expect(row.row_id).toBeDefined(); - expect(typeof row.row_id).toBe("number"); - expect(row.fields).toBeDefined(); - expect(typeof row.fields).toBe("object"); - - if (row.fields.Singular) { - expect(typeof row.fields.Singular).toBe("string"); - expect(row.fields.Singular.length).toBeGreaterThan(0); - } - }); - }, - API_TIMEOUT - ); - }); - - describe("mounts", () => { - it( - "can get a specific mount by ID", - async () => { - const result = await xivapi.mounts.get(1, { - fields: "Singular", - language: "en", - }); - - expect(result).toBeDefined(); - expect(result.row_id).toBeDefined(); - expect(typeof result.row_id).toBe("number"); - expect(result.schema).toBeDefined(); - expect(result.fields).toBeDefined(); - expect(result.row_id).toBe(1); - expect((result.fields as any).Singular).toBe("company chocobo"); - }, - API_TIMEOUT - ); - - it( - "can list mounts with parameters", - async () => { - const result = await xivapi.mounts.list({ limit: 3 }); - - expect(result).toBeDefined(); - expect(result.rows).toBeDefined(); - expect(Array.isArray(result.rows)).toBe(true); - expect(result.schema).toBeDefined(); - expect(result.rows.length).toBeLessThanOrEqual(3); - - result.rows.forEach((row: any) => { - expect(row.row_id).toBeDefined(); - expect(typeof row.row_id).toBe("number"); - expect(row.fields).toBeDefined(); - expect(typeof row.fields).toBe("object"); - - if (row.fields.Name) { - expect(typeof row.fields.Name).toBe("string"); - expect(row.fields.Name.length).toBeGreaterThan(0); - } - }); - }, - API_TIMEOUT - ); - }); - - describe("client options", () => { - it( - "can create client with custom options", - async () => { - const client = new xivapiClient({ - language: "ja" as const, - verbose: true, - version: "latest", - }); - - const result = await client.items.get(1, { - fields: "Name", - }); - - expect(result).toBeDefined(); - expect(result.row_id).toBe(1); - expect((result.fields as any).Name).toBe("ギル"); - }, - API_TIMEOUT - ); - }); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 8674af1..c99404c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,13 +2,13 @@ "extends": "@tsconfig/node22/tsconfig.json", "compilerOptions": { "module": "node20", - "target": "es2020", + "target": "esnext", "types": ["node"], "declaration": true, "preserveConstEnums": true, "outDir": "dist", "rootDir": ".", - "ignoreDeprecations": "6.0", // 'baseUrl' is used in tsup, this is deprecated as of TS 7.0 + "ignoreDeprecations": "6.0" // 'baseUrl' is used in tsup, this is deprecated as of TS 7.0 }, "include": ["src/**/*"], "exclude": ["**/*.spec.ts"] diff --git a/vitest.config.ts b/vitest.config.ts index a419edc..197b164 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,5 +6,6 @@ export default defineConfig({ provider: "v8", reporter: ["text", "json-summary", "lcov", "cobertura"], }, + testTimeout: 10000, }, });