improve api adapters
This commit is contained in:
parent
1298ae1a35
commit
cf34092ccc
9 changed files with 94 additions and 126 deletions
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
import { PartyAdapter } from '../party.adapter'
|
import { PartyAdapter } from '../party.adapter'
|
||||||
import type { Party } from '../party.adapter'
|
import type { Party } from '$lib/types/api/party'
|
||||||
|
|
||||||
describe('PartyAdapter', () => {
|
describe('PartyAdapter', () => {
|
||||||
let adapter: PartyAdapter
|
let adapter: PartyAdapter
|
||||||
|
|
@ -25,26 +25,26 @@ describe('PartyAdapter', () => {
|
||||||
},
|
},
|
||||||
job: {
|
job: {
|
||||||
id: 'job-1',
|
id: 'job-1',
|
||||||
name: { en: 'Warrior' },
|
name: { en: 'Warrior', ja: 'ウォリアー' },
|
||||||
skills: [
|
skills: [
|
||||||
{
|
{
|
||||||
id: 'skill-1',
|
id: 'skill-1',
|
||||||
name: { en: 'Rage' },
|
name: { en: 'Rage', ja: 'レイジ' },
|
||||||
slot: 1
|
slot: 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
raid: {
|
raid: {
|
||||||
id: 'raid-1',
|
id: 'raid-1',
|
||||||
name: { en: 'Proto Bahamut' },
|
name: { en: 'Proto Bahamut', ja: 'プロトバハムート' },
|
||||||
group: {
|
group: {
|
||||||
id: 'group-1',
|
id: 'group-1',
|
||||||
name: { en: 'Tier 1' }
|
name: { en: 'Tier 1', ja: 'ティア1' }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
gridWeapons: [],
|
weapons: [],
|
||||||
gridSummons: [],
|
summons: [],
|
||||||
gridCharacters: [],
|
characters: [],
|
||||||
createdAt: '2024-01-01T00:00:00Z',
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
updatedAt: '2024-01-01T00:00:00Z'
|
updatedAt: '2024-01-01T00:00:00Z'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
* @module adapters/base
|
* @module adapters/base
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { snakeToCamel, camelToSnake } from '../schemas/transforms'
|
|
||||||
import { transformResponse, transformRequest } from '../client'
|
import { transformResponse, transformRequest } from '../client'
|
||||||
import type { AdapterOptions, RequestOptions, AdapterError } from './types'
|
import type { AdapterOptions, RequestOptions, AdapterError } from './types'
|
||||||
import {
|
import {
|
||||||
|
|
@ -90,10 +89,7 @@ export abstract class BaseAdapter {
|
||||||
* })
|
* })
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
protected async request<T>(
|
protected async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||||
path: string,
|
|
||||||
options: RequestOptions = {}
|
|
||||||
): Promise<T> {
|
|
||||||
// Build the full URL with query parameters (support both params and query)
|
// Build the full URL with query parameters (support both params and query)
|
||||||
const url = this.buildURL(path, options.query || options.params)
|
const url = this.buildURL(path, options.query || options.params)
|
||||||
|
|
||||||
|
|
@ -264,7 +260,7 @@ export abstract class BaseAdapter {
|
||||||
*/
|
*/
|
||||||
cancelAll(): void {
|
cancelAll(): void {
|
||||||
// Abort all pending requests
|
// Abort all pending requests
|
||||||
this.abortControllers.forEach(controller => controller.abort())
|
this.abortControllers.forEach((controller) => controller.abort())
|
||||||
this.abortControllers.clear()
|
this.abortControllers.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,8 +286,9 @@ export abstract class BaseAdapter {
|
||||||
let response: Response
|
let response: Response
|
||||||
if (this.options.timeout > 0) {
|
if (this.options.timeout > 0) {
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
const controller = Array.from(this.abortControllers.values())
|
const controller = Array.from(this.abortControllers.values()).find(
|
||||||
.find(c => c.signal === options.signal)
|
(c) => c.signal === options.signal
|
||||||
|
)
|
||||||
controller?.abort()
|
controller?.abort()
|
||||||
}, this.options.timeout)
|
}, this.options.timeout)
|
||||||
|
|
||||||
|
|
@ -349,7 +346,6 @@ export abstract class BaseAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delays execution for a specified duration
|
* Delays execution for a specified duration
|
||||||
* Used for retry backoff
|
* Used for retry backoff
|
||||||
|
|
@ -358,7 +354,7 @@ export abstract class BaseAdapter {
|
||||||
* @returns Promise that resolves after the delay
|
* @returns Promise that resolves after the delay
|
||||||
*/
|
*/
|
||||||
protected delay(ms: number): Promise<void> {
|
protected delay(ms: number): Promise<void> {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms))
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -425,7 +421,7 @@ export abstract class BaseAdapter {
|
||||||
|
|
||||||
// Handle arrays by adding multiple params with the same key
|
// Handle arrays by adding multiple params with the same key
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach(item => {
|
value.forEach((item) => {
|
||||||
url.searchParams.append(key, String(item))
|
url.searchParams.append(key, String(item))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -465,7 +461,7 @@ export abstract class BaseAdapter {
|
||||||
let hash = 0
|
let hash = 0
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
const char = str.charCodeAt(i)
|
const char = str.charCodeAt(i)
|
||||||
hash = ((hash << 5) - hash) + char
|
hash = (hash << 5) - hash + char
|
||||||
hash = hash & hash // Convert to 32bit integer
|
hash = hash & hash // Convert to 32bit integer
|
||||||
}
|
}
|
||||||
return hash.toString(36)
|
return hash.toString(36)
|
||||||
|
|
@ -487,10 +483,11 @@ export abstract class BaseAdapter {
|
||||||
const errorData = await response.json()
|
const errorData = await response.json()
|
||||||
|
|
||||||
// Extract error message from various possible formats
|
// Extract error message from various possible formats
|
||||||
message = errorData.message ||
|
message =
|
||||||
errorData.error ||
|
errorData.message ||
|
||||||
errorData.errors?.[0]?.message ||
|
errorData.error ||
|
||||||
response.statusText
|
errorData.errors?.[0]?.message ||
|
||||||
|
response.statusText
|
||||||
|
|
||||||
details = errorData
|
details = errorData
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -552,4 +549,4 @@ export abstract class BaseAdapter {
|
||||||
this.cache.clear()
|
this.cache.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import type { AdapterError } from './types'
|
||||||
* Extends the native Error class with additional properties
|
* Extends the native Error class with additional properties
|
||||||
*/
|
*/
|
||||||
export class ApiError extends Error implements AdapterError {
|
export class ApiError extends Error implements AdapterError {
|
||||||
name: 'AdapterError' = 'AdapterError'
|
name = 'AdapterError' as const
|
||||||
code: string
|
code: string
|
||||||
status: number
|
status: number
|
||||||
details?: any
|
details?: any
|
||||||
|
|
@ -173,11 +173,7 @@ export class RateLimitError extends ApiError {
|
||||||
* @param details - Additional error details
|
* @param details - Additional error details
|
||||||
* @returns Appropriate error instance based on status code
|
* @returns Appropriate error instance based on status code
|
||||||
*/
|
*/
|
||||||
export function createErrorFromStatus(
|
export function createErrorFromStatus(status: number, message?: string, details?: any): ApiError {
|
||||||
status: number,
|
|
||||||
message?: string,
|
|
||||||
details?: any
|
|
||||||
): ApiError {
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 400:
|
case 400:
|
||||||
return new ApiError('BAD_REQUEST', status, message || 'Bad request', details)
|
return new ApiError('BAD_REQUEST', status, message || 'Bad request', details)
|
||||||
|
|
@ -190,7 +186,9 @@ export function createErrorFromStatus(
|
||||||
|
|
||||||
case 404:
|
case 404:
|
||||||
// Pass the message to NotFoundError if provided
|
// Pass the message to NotFoundError if provided
|
||||||
return message ? new ApiError('NOT_FOUND', 404, message, details) : new NotFoundError(undefined, details)
|
return message
|
||||||
|
? new ApiError('NOT_FOUND', 404, message, details)
|
||||||
|
: new NotFoundError(undefined, details)
|
||||||
|
|
||||||
case 409:
|
case 409:
|
||||||
return new ConflictError(message, details)
|
return new ConflictError(message, details)
|
||||||
|
|
@ -254,7 +252,11 @@ export function isRetryableError(error: any): boolean {
|
||||||
|
|
||||||
// Check by error code (handles both ApiError instances and plain objects)
|
// Check by error code (handles both ApiError instances and plain objects)
|
||||||
// Note: NetworkError sets name to 'NetworkError' but still has AdapterError structure
|
// Note: NetworkError sets name to 'NetworkError' but still has AdapterError structure
|
||||||
if (error instanceof ApiError || error?.name === 'AdapterError' || error?.name === 'NetworkError') {
|
if (
|
||||||
|
error instanceof ApiError ||
|
||||||
|
error?.name === 'AdapterError' ||
|
||||||
|
error?.name === 'NetworkError'
|
||||||
|
) {
|
||||||
const retryableCodes = [
|
const retryableCodes = [
|
||||||
'NETWORK_ERROR',
|
'NETWORK_ERROR',
|
||||||
'TIMEOUT',
|
'TIMEOUT',
|
||||||
|
|
@ -321,11 +323,7 @@ export function normalizeError(error: any): AdapterError {
|
||||||
|
|
||||||
// Generic Error with status
|
// Generic Error with status
|
||||||
if (error?.status) {
|
if (error?.status) {
|
||||||
return createErrorFromStatus(
|
return createErrorFromStatus(error.status, error.message || error.statusText, error).toJSON()
|
||||||
error.status,
|
|
||||||
error.message || error.statusText,
|
|
||||||
error
|
|
||||||
).toJSON()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to generic error
|
// Fallback to generic error
|
||||||
|
|
@ -349,11 +347,12 @@ export function getErrorMessage(error: any): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get message from various error formats
|
// Try to get message from various error formats
|
||||||
const message = error.message ||
|
const message =
|
||||||
error.error ||
|
error.message ||
|
||||||
error.errors?.[0]?.message ||
|
error.error ||
|
||||||
error.statusText ||
|
error.errors?.[0]?.message ||
|
||||||
'An unknown error occurred'
|
error.statusText ||
|
||||||
|
'An unknown error occurred'
|
||||||
|
|
||||||
// Make network errors more user-friendly
|
// Make network errors more user-friendly
|
||||||
if (message.includes('NetworkError') || message.includes('Failed to fetch')) {
|
if (message.includes('NetworkError') || message.includes('Failed to fetch')) {
|
||||||
|
|
|
||||||
|
|
@ -9,62 +9,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BaseAdapter } from './base.adapter'
|
import { BaseAdapter } from './base.adapter'
|
||||||
import type { RequestOptions, AdapterOptions, PaginatedResponse } from './types'
|
import type { AdapterOptions, PaginatedResponse } from './types'
|
||||||
import { DEFAULT_ADAPTER_CONFIG } from './config'
|
import { DEFAULT_ADAPTER_CONFIG } from './config'
|
||||||
import type { Party, GridWeapon, GridCharacter, GridSummon } from '$lib/types/api/party'
|
import type { Party, GridWeapon, GridCharacter, GridSummon } from '$lib/types/api/party'
|
||||||
|
|
||||||
/**
|
|
||||||
* Party data structure
|
|
||||||
*/
|
|
||||||
export interface Party {
|
|
||||||
id: string
|
|
||||||
shortcode: string
|
|
||||||
name?: string
|
|
||||||
description?: string
|
|
||||||
visibility: 'public' | 'private' | 'unlisted'
|
|
||||||
user: {
|
|
||||||
id: string
|
|
||||||
username: string
|
|
||||||
}
|
|
||||||
job?: {
|
|
||||||
id: string
|
|
||||||
name: Record<string, string>
|
|
||||||
skills: Array<{
|
|
||||||
id: string
|
|
||||||
name: Record<string, string>
|
|
||||||
slot: number
|
|
||||||
}>
|
|
||||||
accessory?: {
|
|
||||||
id: string
|
|
||||||
name: Record<string, string>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
raid?: {
|
|
||||||
id: string
|
|
||||||
name: Record<string, string>
|
|
||||||
group?: {
|
|
||||||
id: string
|
|
||||||
name: Record<string, string>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
weapons: GridWeapon[]
|
|
||||||
summons: GridSummon[]
|
|
||||||
characters: GridCharacter[]
|
|
||||||
guidebook?: {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
}
|
|
||||||
extras?: Record<string, any>
|
|
||||||
createdAt: string
|
|
||||||
updatedAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// GridWeapon type is imported from types/api/party
|
|
||||||
|
|
||||||
// GridSummon type is imported from types/api/party
|
|
||||||
|
|
||||||
// GridCharacter type is imported from types/api/party
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parameters for creating a new party
|
* Parameters for creating a new party
|
||||||
*/
|
*/
|
||||||
|
|
@ -261,10 +209,7 @@ export class PartyAdapter extends BaseAdapter {
|
||||||
/**
|
/**
|
||||||
* Updates the job for a party
|
* Updates the job for a party
|
||||||
*/
|
*/
|
||||||
async updateJob(
|
async updateJob(shortcode: string, jobId: string): Promise<Party> {
|
||||||
shortcode: string,
|
|
||||||
jobId: string
|
|
||||||
): Promise<Party> {
|
|
||||||
return this.request<Party>(`/parties/${shortcode}/jobs`, {
|
return this.request<Party>(`/parties/${shortcode}/jobs`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: {
|
body: {
|
||||||
|
|
@ -277,25 +222,45 @@ export class PartyAdapter extends BaseAdapter {
|
||||||
* Updates job skills for a party
|
* Updates job skills for a party
|
||||||
*/
|
*/
|
||||||
async updateJobSkills(
|
async updateJobSkills(
|
||||||
shortcode: string,
|
partyId: string,
|
||||||
skills: Array<{ id: string; slot: number }>
|
skills: Array<{ id: string; slot: number }>
|
||||||
): Promise<Party> {
|
): Promise<Party> {
|
||||||
return this.request<Party>(`/parties/${shortcode}/job_skills`, {
|
console.log('[updateJobSkills] Input skills array:', skills)
|
||||||
|
|
||||||
|
// Convert skills array to Rails expected format
|
||||||
|
const party: Record<string, string | null> = {}
|
||||||
|
|
||||||
|
// Initialize all slots with null
|
||||||
|
for (let i = 1; i <= 4; i++) {
|
||||||
|
party[`skill${i}_id`] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the provided skills
|
||||||
|
skills.forEach(skill => {
|
||||||
|
// Rails expects skill1_id, skill2_id, skill3_id, skill4_id
|
||||||
|
party[`skill${skill.slot + 1}_id`] = skill.id
|
||||||
|
})
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
party
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[updateJobSkills] Sending to server:', {
|
||||||
|
url: `/parties/${partyId}/job_skills`,
|
||||||
|
body: requestBody
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.request<Party>(`/parties/${partyId}/job_skills`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: {
|
body: requestBody
|
||||||
skills
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a job skill from a party
|
* Removes a job skill from a party
|
||||||
*/
|
*/
|
||||||
async removeJobSkill(
|
async removeJobSkill(partyId: string, skillSlot: number): Promise<Party> {
|
||||||
shortcode: string,
|
return this.request<Party>(`/parties/${partyId}/job_skills`, {
|
||||||
skillSlot: number
|
|
||||||
): Promise<Party> {
|
|
||||||
return this.request<Party>(`/parties/${shortcode}/job_skills`, {
|
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
body: {
|
body: {
|
||||||
slot: skillSlot
|
slot: skillSlot
|
||||||
|
|
@ -310,7 +275,7 @@ export class PartyAdapter extends BaseAdapter {
|
||||||
return this.request<Blob>(`/parties/${shortcode}/preview`, {
|
return this.request<Blob>(`/parties/${shortcode}/preview`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'image/png'
|
Accept: 'image/png'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -377,4 +342,4 @@ export class PartyAdapter extends BaseAdapter {
|
||||||
/**
|
/**
|
||||||
* Default party adapter instance
|
* Default party adapter instance
|
||||||
*/
|
*/
|
||||||
export const partyAdapter = new PartyAdapter(DEFAULT_ADAPTER_CONFIG)
|
export const partyAdapter = new PartyAdapter(DEFAULT_ADAPTER_CONFIG)
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ export interface InfiniteScrollOptions<T> {
|
||||||
* @example
|
* @example
|
||||||
* ```svelte
|
* ```svelte
|
||||||
* <script>
|
* <script>
|
||||||
* import { createInfiniteScrollResource } from '$lib/api/adapters/resources'
|
* import { createInfiniteScrollResource } from '$lib/api/adapters/resources/infiniteScroll.resource.svelte'
|
||||||
* import { partyAdapter } from '$lib/api/adapters'
|
* import { partyAdapter } from '$lib/api/adapters/party.adapter'
|
||||||
*
|
*
|
||||||
* const resource = createInfiniteScrollResource({
|
* const resource = createInfiniteScrollResource({
|
||||||
* fetcher: (page) => partyAdapter.list({ page }),
|
* fetcher: (page) => partyAdapter.list({ page }),
|
||||||
|
|
@ -243,7 +243,7 @@ export class InfiniteScrollResource<T> {
|
||||||
} finally {
|
} finally {
|
||||||
this.loadingMore = false
|
this.loadingMore = false
|
||||||
if (this.abortController) {
|
if (this.abortController) {
|
||||||
this.abortController = undefined
|
delete this.abortController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
* @module adapters/resources/party
|
* @module adapters/resources/party
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PartyAdapter, partyAdapter, type Party, type CreatePartyParams, type UpdatePartyParams } from '../party.adapter'
|
import { PartyAdapter, partyAdapter, type CreatePartyParams, type UpdatePartyParams } from '../party.adapter'
|
||||||
|
import type { Party } from '$lib/types/api/party'
|
||||||
import type { AdapterError } from '../types'
|
import type { AdapterError } from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -48,7 +49,7 @@ interface PartyListState {
|
||||||
* @example
|
* @example
|
||||||
* ```svelte
|
* ```svelte
|
||||||
* <script>
|
* <script>
|
||||||
* import { createPartyResource } from '$lib/api/adapters/resources'
|
* import { createPartyResource } from '$lib/api/adapters/resources/party.resource.svelte'
|
||||||
*
|
*
|
||||||
* const party = createPartyResource()
|
* const party = createPartyResource()
|
||||||
*
|
*
|
||||||
|
|
@ -299,7 +300,7 @@ export class PartyResource {
|
||||||
* Updates the job for the current party
|
* Updates the job for the current party
|
||||||
*/
|
*/
|
||||||
async updateJob(
|
async updateJob(
|
||||||
shortcode: string,
|
partyId: string,
|
||||||
jobId: string,
|
jobId: string,
|
||||||
skills?: Array<{ id: string; slot: number }>,
|
skills?: Array<{ id: string; slot: number }>,
|
||||||
accessoryId?: string
|
accessoryId?: string
|
||||||
|
|
@ -307,7 +308,16 @@ export class PartyResource {
|
||||||
this.current = { ...this.current, updating: true, error: undefined }
|
this.current = { ...this.current, updating: true, error: undefined }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const party = await this.adapter.updateJob(shortcode, jobId, skills, accessoryId)
|
// Update job first
|
||||||
|
let party = await this.adapter.updateJob(partyId, jobId)
|
||||||
|
|
||||||
|
// Update skills if provided
|
||||||
|
if (skills) {
|
||||||
|
party = await this.adapter.updateJobSkills(partyId, skills)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle accessory update when API supports it
|
||||||
|
|
||||||
this.current = { data: party, loading: false, updating: false }
|
this.current = { data: party, loading: false, updating: false }
|
||||||
return party
|
return party
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ interface SearchState {
|
||||||
* @example
|
* @example
|
||||||
* ```svelte
|
* ```svelte
|
||||||
* <script>
|
* <script>
|
||||||
* import { createSearchResource } from '$lib/api/adapters/resources'
|
* import { createSearchResource } from '$lib/api/adapters/resources/search.resource.svelte'
|
||||||
*
|
*
|
||||||
* const search = createSearchResource({
|
* const search = createSearchResource({
|
||||||
* debounceMs: 300,
|
* debounceMs: 300,
|
||||||
|
|
|
||||||
|
|
@ -359,4 +359,4 @@ export interface ResourceOptions<T> {
|
||||||
|
|
||||||
/** Dependencies that trigger refetch when changed */
|
/** Dependencies that trigger refetch when changed */
|
||||||
dependencies?: any[]
|
dependencies?: any[]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -321,11 +321,8 @@ const GridCharacterSchema = z.object({
|
||||||
perpetuity: z.boolean().nullish().default(false),
|
perpetuity: z.boolean().nullish().default(false),
|
||||||
|
|
||||||
// Rings and earring
|
// Rings and earring
|
||||||
ring1: RingSchema,
|
over_mastery: z.array(RingSchema).nullish(),
|
||||||
ring2: RingSchema,
|
aetherial_mastery: RingSchema.nullish(),
|
||||||
ring3: RingSchema,
|
|
||||||
ring4: RingSchema,
|
|
||||||
earring: RingSchema,
|
|
||||||
|
|
||||||
// Awakening
|
// Awakening
|
||||||
awakening_id: z.string().nullish(),
|
awakening_id: z.string().nullish(),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue