diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..81274ea --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = tab +insert_final_newline = true +max_line_length = 80 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 +indent_style = space diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7379d4d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI +on: + - push +jobs: + check-eslint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v1 + - run: bun install + - run: bun run check:eslint + check-knip: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v1 + - run: bun install + - run: bun run check:knip + check-prettier: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v1 + - run: bun install + - run: bun run check:prettier + check-publint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v1 + - run: bun install + - run: bun run check:publint + validate-schemas: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v1 + - run: bun install + - run: bun run start --filter=schemas diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ac1391 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# global +dist/ + +# per-package + +packages/schemas/data/*.json + +################################################################################ + +# bun +node_modules/ + +# ide:intellij +.idea + +# os:macOS +.DS_Store + +# turborepo +.turbo/ + +# typescript +*.tsbuildinfo diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c11532c --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig", + "esbenp.prettier-vscode", + "Vercel.turbo-vsc" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3fef70a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,14 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always" + }, + "editor.formatOnSave": true, + "eslint.validate": [ + "javascript", + "javascriptreact", + "json", + "jsonc", + "typescript", + "typescriptreact" + ] +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6e45099 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) `2024` Florian Wendelborn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..efa8709 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# Skyblock Finance Opensource + +This repo contains the opensource parts of + +## Contributing + +1. Install [`bun`](https://bun.sh) +2. `bun install` +3. `bun run test` +4. Make your changes and submit a pull request diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..97c0d15 Binary files /dev/null and b/bun.lockb differ diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..372ee01 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,53 @@ +import tseslint from 'typescript-eslint' +import eslintConfig from '@skyblock-finance/eslint-config' +import { fileURLToPath } from 'node:url' +import path from 'node:path' + +const root = (() => { + const cause = [] + + try { + // eslint-disable-next-line no-undef + const result = __dirname + if (result) return result + } catch (error) { + cause.push(error) + } + + try { + const result = import.meta.dirname + if (result) return result + } catch (error) { + cause.push(error) + } + + try { + const result = path.dirname(fileURLToPath(import.meta.url)) + if (result) return result + } catch (error) { + cause.push(error) + } + + throw new Error('could not determine project root', { cause }) +})() + +export default tseslint.config( + /** + * DO NOT ADD ANY OTHER KEYS TO THIS FIRST OBJECT + * + * @see {@link https://eslint.org/docs/latest/use/configure/ignore#ignoring-files} + */ + { + ignores: ['**/dist/**', '**/.turbo/**', 'packages/schemas/data/*.json'], + }, + { + extends: [...eslintConfig.configs.default], + languageOptions: { + parserOptions: { + project: ['./tsconfig.json', './packages/*/tsconfig.json'], + tsconfigRootDir: root, + }, + }, + }, + ...eslintConfig.configs.json, +) diff --git a/internals/eslint-config/package.json b/internals/eslint-config/package.json new file mode 100644 index 0000000..250b96a --- /dev/null +++ b/internals/eslint-config/package.json @@ -0,0 +1,51 @@ +{ + "bugs": { + "url": "https://github.com/skyblock-finance/skyblock-finance-opensource/issues" + }, + "dependencies": { + "@eslint/js": "^9.0.0", + "eslint-plugin-jsonc": "^2.15.0", + "jsonc-eslint-parser": "^2.4.0", + "typescript-eslint": "^7.6.0" + }, + "description": "Skyblock Finance Shared ESLint Config", + "devDependencies": { + "@types/eslint__js": "^8.42.3", + "@typescript-eslint/utils": "^7.6.0" + }, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist/**/!(*.tsbuildinfo)" + ], + "homepage": "https://github.com/skyblock-finance/skyblock-finance-opensource/tree/master/packages/eslint-config", + "keywords": [ + "eslint", + "eslint-config" + ], + "license": "MIT", + "module": "./dist/index.js", + "name": "@skyblock-finance/eslint-config", + "peerDependencies": { + "eslint": ">= 9", + "typescript": ">= 4" + }, + "repository": "git+https://github.com/skyblock-finance/skyblock-finance-opensource.git", + "scripts": { + "build": "tsc --build", + "check:eslint": "bun run eslint --max-warnings=0 .", + "check:prettier": "bun --bun run --cwd ../.. prettier --check internals/eslint-config", + "check:publint": "bun --bun run publint", + "fix:eslint": "bun run check:eslint -- --fix", + "fix:prettier": "bun --bun run check:prettier -- --write" + }, + "type": "module", + "types": "./dist/index.d.ts", + "version": "0.0.1" +} diff --git a/internals/eslint-config/source/index.ts b/internals/eslint-config/source/index.ts new file mode 100644 index 0000000..660849f --- /dev/null +++ b/internals/eslint-config/source/index.ts @@ -0,0 +1,85 @@ +import eslint from '@eslint/js' +import tseslint from 'typescript-eslint' +import jsonc from 'eslint-plugin-jsonc' +import jsoncEslintParser from 'jsonc-eslint-parser' +import { TSESLint } from '@typescript-eslint/utils' + +export default { + configs: { + default: tseslint.config({ + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + ], + ignores: ['**/*.json'], + languageOptions: { + parser: tseslint.parser, + }, + linterOptions: { + reportUnusedDisableDirectives: 'error', + }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + args: 'all', + argsIgnorePattern: '^_', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + }, + }), + json: tseslint.config( + { + files: ['**/*.json'], + ignores: ['**/tsconfig*.json'], + languageOptions: { + parser: jsoncEslintParser, + }, + plugins: { + jsonc, + }, + rules: { + ...(jsonc.configs['recommended-with-json'] + .rules as TSESLint.FlatConfig.Rules), + 'jsonc/sort-keys': [ + 'warn', + { + pathPattern: '^exports(?:\\[[^\\]]+\\]|\\.[^.]+)+$', + order: ['types', 'default', 'import', 'require'], + }, + { + pathPattern: '.*', + order: { type: 'asc' }, + }, + ], + }, + }, + { + files: ['**/tsconfig*.json'], + languageOptions: { + parser: jsoncEslintParser, + }, + plugins: { + jsonc, + }, + rules: { + ...(jsonc.configs['recommended-with-jsonc'] + .rules as TSESLint.FlatConfig.Rules), + 'jsonc/sort-keys': [ + 'warn', + { + pathPattern: '.*', + order: { type: 'asc' }, + }, + ], + }, + }, + ), + }, +} diff --git a/internals/eslint-config/tsconfig.json b/internals/eslint-config/tsconfig.json new file mode 100644 index 0000000..4076005 --- /dev/null +++ b/internals/eslint-config/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "lib": ["ES2022"], + "module": "ESNext", + "moduleResolution": "node", + "noImplicitAny": true, + "noUncheckedIndexedAccess": true, + "outDir": "dist", + "resolveJsonModule": true, + "rootDir": "source", + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "ESNext", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + }, + "exclude": ["node_modules", "dist"], + "include": ["source/*.ts", "source/**/*.ts"] +} diff --git a/internals/eslint-config/turbo.json b/internals/eslint-config/turbo.json new file mode 100644 index 0000000..881bce0 --- /dev/null +++ b/internals/eslint-config/turbo.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "pipeline": { + "build": { + "inputs": ["./scripts/**", "./source/**", "tsconfig.*"], + "outputMode": "new-only", + "outputs": ["./dist/**"] + } + } +} diff --git a/internals/fake-root/package.json b/internals/fake-root/package.json new file mode 100644 index 0000000..f174e6d --- /dev/null +++ b/internals/fake-root/package.json @@ -0,0 +1,12 @@ +{ + "name": "fake-root", + "private": true, + "scripts": { + "check:eslint": "bun run --cwd ../.. eslint --max-warnings=0 --ignore-pattern=packages --ignore-pattern=internals .", + "check:knip": "bun run --cwd ../.. knip", + "check:prettier": "bun --bun run --cwd ../.. prettier --check .", + "fix:eslint": "yarn run check:eslint --fix", + "fix:prettier": "bun --bun run check:prettier --write" + }, + "version": "0.0.0" +} diff --git a/internals/fake-root/readme.md b/internals/fake-root/readme.md new file mode 100644 index 0000000..2160dc5 --- /dev/null +++ b/internals/fake-root/readme.md @@ -0,0 +1,5 @@ +# Fake Root + +This package exists in order to lint the repo root with the same turborepo workflows we can use for ordinary packages. + +See https://turbo.build/messages/missing-root-task-in-turbo-json for context as to why this causes an infinite loop without this “root” package. diff --git a/internals/fake-root/turbo.json b/internals/fake-root/turbo.json new file mode 100644 index 0000000..dc3b68d --- /dev/null +++ b/internals/fake-root/turbo.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "pipeline": { + "check:eslint": { + "inputs": [ + "../../**/*.{cjs,js,mjs,json,mjs,mts,ts}", + "!../../internals/**/*", + "!../../packages/**/*", + "../../eslint.config.mjs" + ], + "outputMode": "new-only", + "outputs": ["./dist/**"] + }, + "check:prettier": { + "inputs": [ + "../../**/*.{cjs,js,mjs,json,mjs,mts,ts}", + "!../../packages/**/*", + "../../eslint.config.mjs" + ], + "outputMode": "new-only", + "outputs": ["./dist/**"] + }, + "download": {}, + "fix:eslint": { + "inputs": [ + "../../**/*.{cjs,js,mjs,json,mjs,mts,ts}", + "!../../internals/**/*", + "!../../packages/**/*", + "../../eslint.config.mjs" + ], + "outputMode": "new-only", + "outputs": ["./dist/**"] + }, + "fix:prettier": { + "inputs": [ + "../../**/*.{cjs,js,mjs,json,mjs,mts,ts}", + "!../../packages/**/*", + "../../eslint.config.mjs" + ], + "outputMode": "new-only", + "outputs": ["./dist/**"] + }, + "watch": { + "cache": false, + "dependsOn": ["^build", "download"], + "persistent": true + } + } +} diff --git a/knip.json b/knip.json new file mode 100644 index 0000000..3a749b0 --- /dev/null +++ b/knip.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://unpkg.com/knip@5/schema.json", + "ignore": ["dist/**"], + "ignoreBinaries": ["prettier", "publint"], + "workspaces": { + "internals/*": { + "entry": ["scripts/*.ts", "source/index.ts"], + "project": ["source/**/*.ts"] + }, + "packages/*": { + "entry": ["scripts/*.ts", "source/index.ts"], + "project": ["source/**/*.ts"] + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d4391de --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "devDependencies": { + "@skyblock-finance/eslint-config": "*", + "@types/bun": "latest", + "eslint": "^9.0.0", + "knip": "^5.9.4", + "nodemon": "^3.1.0", + "prettier": "^3.2.5", + "publint": "^0.2.7", + "turbo": "^1.13.2", + "typescript": "^5.4.5", + "typescript-eslint": "^7.7.0" + }, + "license": "MIT", + "name": "root", + "peerDependencies": { + "typescript": "^5.0.0" + }, + "private": true, + "scripts": { + "build": "turbo run build", + "check": "turbo run check", + "check:eslint": "turbo run check:eslint", + "check:knip": "turbo run check:knip", + "check:prettier": "turbo run check:prettier", + "check:publint": "turbo run check:publint", + "fix": "turbo run fix", + "fix:eslint": "turbo run fix:eslint", + "fix:prettier": "turbo run fix:prettier", + "test": "turbo run test" + }, + "type": "module", + "workspaces": [ + "internals/*", + "packages/*" + ] +} diff --git a/packages/schemas/data/.gitkeep b/packages/schemas/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/schemas/package.json b/packages/schemas/package.json new file mode 100644 index 0000000..a2f7210 --- /dev/null +++ b/packages/schemas/package.json @@ -0,0 +1,38 @@ +{ + "dependencies": { + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/json-stable-stringify": "^1.0.36", + "@types/lodash": "^4.14.188", + "@types/node": "^18.11.9", + "json-stable-stringify": "^1.1.1", + "lodash": "^4.17.21", + "tslog": "^4.9.2" + }, + "files": [ + "dist", + "source" + ], + "license": "MIT", + "main": "dist/index.js", + "name": "@skyblock-finance/schemas", + "peerDependencies": { + "typescript": ">= 4" + }, + "private": true, + "scripts": { + "build": "tsc --build", + "check:eslint": "bun run eslint --max-warnings=0 .", + "check:prettier": "bun --bun run --cwd ../.. prettier --check packages/schemas", + "check:publint": "bun --bun run publint", + "download": "bun scripts/download-data.ts", + "fix:eslint": "bun run check:eslint --fix", + "fix:prettier": "bun --bun run check:prettier --write", + "start": "bun scripts/check-all.ts", + "watch": "nodemon -e ts,json --watch source --watch scripts --exec \"bun run start\"" + }, + "type": "commonjs", + "types": "dist/index.d.ts", + "version": "0.0.1" +} diff --git a/packages/schemas/scripts/check-all.ts b/packages/schemas/scripts/check-all.ts new file mode 100644 index 0000000..0920f3e --- /dev/null +++ b/packages/schemas/scripts/check-all.ts @@ -0,0 +1,159 @@ +import fs from 'fs/promises' +import path from 'path' +import jsonStableStringify from 'json-stable-stringify' + +import _ from 'lodash' +import { Logger } from 'tslog' +import { z } from 'zod' + +import { + bazaarResponseSchemaRuntime, + bazaarResponseSchemaStrict, +} from '../source/api/bazaar' +import { + electionResponseSchemaRuntime, + electionResponseSchemaStrict, +} from '../source/api/election' +import { + itemsResponseSchemaRuntime, + itemsResponseSchemaStrict, +} from '../source/api/items' +import { + skillsResponseSchemaRuntime, + skillsResponseSchemaStrict, +} from '../source/api/skills' + +export const log = new Logger() + +const TO_CHECK: { + file: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getMessage: (result: any) => string + isEnabled: boolean + basicSchema?: z.ZodSchema + strictSchema: z.ZodSchema + runtimeSchema: z.ZodSchema +}[] = [ + { + file: 'bazaar.json', + getMessage: (result: z.output) => + `found ${Object.keys(result.products).length} items last updated at ${result.lastUpdated.toISOString()}`, + isEnabled: true, + runtimeSchema: bazaarResponseSchemaRuntime, + strictSchema: bazaarResponseSchemaStrict, + }, + { + file: 'election.json', + getMessage: (result: z.output) => + `found current mayor ${result.mayor.name} and election for year ${ + result.current?.year ?? 'N/A' + }`, + isEnabled: true, + runtimeSchema: electionResponseSchemaRuntime, + strictSchema: electionResponseSchemaStrict, + }, + { + file: 'items.json', + getMessage: (result: z.output) => + `found ${result.items.length} items`, + isEnabled: true, + runtimeSchema: itemsResponseSchemaRuntime, + strictSchema: itemsResponseSchemaStrict, + }, + { + file: 'skills.json', + getMessage: (result: z.output) => + `found skills ${JSON.stringify(Object.keys(result.skills))}`, + isEnabled: true, + runtimeSchema: skillsResponseSchemaRuntime, + strictSchema: skillsResponseSchemaStrict, + }, +] + +const checkSchema = async ({ + file, + getMessage, + json, + schema, + type, +}: { + file: string + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getMessage(result: any): string + json: unknown + schema: z.ZodSchema + type: string +}) => { + const fileName = file.replace('.json', `.parsed.${type}.json`) + + log.info(`${fileName}: parsing zod schema`) + const resultRuntime = await schema.parseAsync(json) + + log.info(`${fileName}: all good (${getMessage(resultRuntime)})`) + log.info(`${fileName}: writing parsed data`) + await fs.writeFile( + path.join(__dirname, '..', 'data', fileName), + jsonStableStringify(resultRuntime, { space: '\t' }), + ) +} + +for (const toCheck of TO_CHECK) { + if (!toCheck.isEnabled) { + log.warn(`${toCheck.file} is disabled, skipping`) + continue + } + + log.info(`${toCheck.file}: loading`) + const content = await fs.readFile( + path.join(__dirname, '..', 'data', toCheck.file), + ) + log.info(`${toCheck.file}: parsing JSON`) + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const json = JSON.parse(content.toString()) + + try { + if (toCheck.basicSchema) + await checkSchema({ + file: toCheck.file, + getMessage: toCheck.getMessage, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + json, + schema: toCheck.basicSchema, + type: 'basic', + }) + await checkSchema({ + file: toCheck.file, + getMessage: toCheck.getMessage, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + json, + schema: toCheck.runtimeSchema, + type: 'runtime', + }) + await checkSchema({ + file: toCheck.file, + getMessage: toCheck.getMessage, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + json, + schema: toCheck.strictSchema, + type: 'strict', + }) + } catch (error) { + const e = error as z.ZodError + + if (e.issues) { + log.error( + e.issues + .map((i) => ({ + ...i, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + '=': _.get(json, i.path), + })) + .filter((_, index) => index < 3), + ) + + log.info(`failed with ${e.issues.length} issues`) + } else log.error(error) + + process.exit(1) + } +} diff --git a/packages/schemas/scripts/download-data.ts b/packages/schemas/scripts/download-data.ts new file mode 100644 index 0000000..853cf9e --- /dev/null +++ b/packages/schemas/scripts/download-data.ts @@ -0,0 +1,67 @@ +import fs from 'fs/promises' +import path from 'path' + +import jsonStableStringify from 'json-stable-stringify' +import { Logger } from 'tslog' + +export const log = new Logger() + +interface Common { + name: string +} + +const FILES: ( + | (Common & { + type: 'simple' + url: string + }) + | (Common & { + fetch(): Promise + type: 'custom' + }) +)[] = [ + { + name: 'bazaar.json', + type: 'simple', + url: 'https://api.hypixel.net/skyblock/bazaar', + }, + { + name: 'election.json', + type: 'simple', + url: 'https://api.hypixel.net/resources/skyblock/election', + }, + { + name: 'items.json', + type: 'simple', + url: 'https://api.hypixel.net/resources/skyblock/items', + }, + { + name: 'skills.json', + type: 'simple', + url: 'https://api.hypixel.net/resources/skyblock/skills', + }, +] + +for (const file of FILES) { + const data = await (async () => { + switch (file.type) { + case 'simple': { + log.info(`${file.name}: downloading ${file.url}`) + return await fetch(file.url).then((request) => request.json()) + } + + case 'custom': + log.info(`${file.name}: downloading with custom fetcher`) + return await file.fetch() + } + })() + + log.info(`${file.name}: sorting JSON`) + + log.info(`${file.name}: writing to ${file.name}`) + + await fs.writeFile( + path.join(__dirname, '..', 'data', file.name), + jsonStableStringify(data, { space: '\t' }), + ) +} diff --git a/packages/schemas/source/api/bazaar/common.ts b/packages/schemas/source/api/bazaar/common.ts new file mode 100644 index 0000000..74a4317 --- /dev/null +++ b/packages/schemas/source/api/bazaar/common.ts @@ -0,0 +1,31 @@ +import { z } from 'zod' + +export const UPPER_SNAKE_CASE_REGEX = /[A-Z]+(_[A-Z]+)*/ + +export const summarySchema = z.object({ + amount: z.number().int(), + orders: z.number().int(), + pricePerUnit: z.number(), +}) + +export const quickStatusSchema = z.object({ + buyMovingWeek: z.number().int(), + buyOrders: z.number().int(), + buyPrice: z.number(), + buyVolume: z.number().int(), + productId: z.string().regex(UPPER_SNAKE_CASE_REGEX), + sellMovingWeek: z.number().int(), + sellOrders: z.number().int(), + sellPrice: z.number(), + sellVolume: z.number().int(), +}) + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const removeUselessItems = >(data: T): T => + Object.fromEntries( + // remove weird useless items from response + Object.entries(data).filter( + ([key]) => + key !== 'BAZAAR_COOKIE' && key !== 'ENCHANTED_CARROT_ON_A_STICK', + ), + ) as T diff --git a/packages/schemas/source/api/bazaar/index.ts b/packages/schemas/source/api/bazaar/index.ts new file mode 100644 index 0000000..7aca060 --- /dev/null +++ b/packages/schemas/source/api/bazaar/index.ts @@ -0,0 +1,2 @@ +export * from './strict' +export * from './runtime' diff --git a/packages/schemas/source/api/bazaar/runtime.ts b/packages/schemas/source/api/bazaar/runtime.ts new file mode 100644 index 0000000..51d3f09 --- /dev/null +++ b/packages/schemas/source/api/bazaar/runtime.ts @@ -0,0 +1,19 @@ +import { z } from 'zod' + +import { apiResponseSchema } from '../../common' + +import { quickStatusSchema, removeUselessItems, summarySchema } from './common' + +const bazaarProductSchema = z.object({ + buy_summary: z.array(summarySchema), + product_id: z.string(), + quick_status: quickStatusSchema.pick({ + buyMovingWeek: true, + sellMovingWeek: true, + }), + sell_summary: z.array(summarySchema), +}) + +export const bazaarResponseSchemaRuntime = apiResponseSchema.extend({ + products: z.record(bazaarProductSchema).transform(removeUselessItems), +}) diff --git a/packages/schemas/source/api/bazaar/strict.ts b/packages/schemas/source/api/bazaar/strict.ts new file mode 100644 index 0000000..afb9a85 --- /dev/null +++ b/packages/schemas/source/api/bazaar/strict.ts @@ -0,0 +1,25 @@ +import { z } from 'zod' + +import { apiResponseSchema } from '../../common' + +import { + quickStatusSchema, + removeUselessItems, + summarySchema, + UPPER_SNAKE_CASE_REGEX, +} from './common' + +const bazaarProductSchema = z + .object({ + buy_summary: z.array(summarySchema.strict()), + product_id: z.string().regex(UPPER_SNAKE_CASE_REGEX), + quick_status: quickStatusSchema, + sell_summary: z.array(summarySchema.strict()), + }) + .strict() + +export const bazaarResponseSchemaStrict = apiResponseSchema + .extend({ + products: z.record(bazaarProductSchema).transform(removeUselessItems), + }) + .strict() diff --git a/packages/schemas/source/api/election/common.ts b/packages/schemas/source/api/election/common.ts new file mode 100644 index 0000000..f1d8ce9 --- /dev/null +++ b/packages/schemas/source/api/election/common.ts @@ -0,0 +1,8 @@ +import { z } from 'zod' + +export const perkSchema = z.object({ + description: z.string(), + name: z.string(), +}) + +export const yearSchema = z.number().int() diff --git a/packages/schemas/source/api/election/index.ts b/packages/schemas/source/api/election/index.ts new file mode 100644 index 0000000..bb1c175 --- /dev/null +++ b/packages/schemas/source/api/election/index.ts @@ -0,0 +1,2 @@ +export * from './runtime' +export * from './strict' diff --git a/packages/schemas/source/api/election/runtime.ts b/packages/schemas/source/api/election/runtime.ts new file mode 100644 index 0000000..c5d7712 --- /dev/null +++ b/packages/schemas/source/api/election/runtime.ts @@ -0,0 +1,33 @@ +import { z } from 'zod' + +import { apiResponseSchema } from '../../common' + +import { perkSchema, yearSchema } from './common' + +const candidateSchema = z.object({ + key: z.string(), + name: z.string(), + perks: z.array(perkSchema), +}) + +export const electionResponseSchemaRuntime = apiResponseSchema.extend({ + current: z + .object({ + candidates: z.array(candidateSchema), + year: yearSchema, + }) + .optional(), + mayor: z.object({ + election: z.object({ + candidates: z.array( + candidateSchema.extend({ + votes: z.number(), + }), + ), + year: yearSchema, + }), + key: z.string(), + name: z.string(), + perks: z.array(perkSchema), + }), +}) diff --git a/packages/schemas/source/api/election/strict.ts b/packages/schemas/source/api/election/strict.ts new file mode 100644 index 0000000..a68bdf6 --- /dev/null +++ b/packages/schemas/source/api/election/strict.ts @@ -0,0 +1,58 @@ +import { z } from 'zod' + +import { apiResponseSchema } from '../../common' + +import { perkSchema, yearSchema } from './common' + +enum MayorKey { + DERP = 'derp', + DUNGEONS = 'dungeons', + ECONOMIST = 'economist', + EVENTS = 'events', + FARMING = 'farming', + FISHING = 'fishing', + JERRY = 'jerry', + MINING = 'mining', + PETS = 'pets', + SHADY = 'shady', + SLAYER = 'slayer', + WIZARD = 'wizard', +} + +const candidateSchema = z + .object({ + key: z.nativeEnum(MayorKey), + name: z.string(), + perks: z.array(perkSchema.strict()), + votes: z.number().int(), + }) + .strict() + +export const electionResponseSchemaStrict = apiResponseSchema + .extend({ + current: z + .object({ + candidates: z.array(candidateSchema), + year: yearSchema, + }) + .strict() + .optional(), + mayor: z + .object({ + election: z + .object({ + candidates: z.array( + candidateSchema.extend({ + votes: z.number(), + }), + ), + year: yearSchema, + }) + .strict(), + key: z.nativeEnum(MayorKey), + name: z.string(), + perks: z.array(perkSchema.strict()), + }) + .strict(), + }) + .strict() diff --git a/packages/schemas/source/api/index.ts b/packages/schemas/source/api/index.ts new file mode 100644 index 0000000..7747162 --- /dev/null +++ b/packages/schemas/source/api/index.ts @@ -0,0 +1,5 @@ +export * as bazaar from './bazaar' +export * as common from '../common' +export * as election from './election' +export * as skills from './skills' +export * as items from './items' diff --git a/packages/schemas/source/api/items/common.ts b/packages/schemas/source/api/items/common.ts new file mode 100644 index 0000000..99c1f4d --- /dev/null +++ b/packages/schemas/source/api/items/common.ts @@ -0,0 +1,17 @@ +import { z } from 'zod' + +import { urlTransformHttps } from '../../utilities/transforms/url-transform-https' +import { base64JsonToObject } from '../../utilities/preprocessors/base64-json-to-object' + +export const skinSchema = z + .preprocess( + base64JsonToObject, + z.object({ + textures: z.object({ + SKIN: z.object({ + url: urlTransformHttps, + }), + }), + }), + ) + .transform((x) => x.textures.SKIN.url) diff --git a/packages/schemas/source/api/items/index.ts b/packages/schemas/source/api/items/index.ts new file mode 100644 index 0000000..7aca060 --- /dev/null +++ b/packages/schemas/source/api/items/index.ts @@ -0,0 +1,2 @@ +export * from './strict' +export * from './runtime' diff --git a/packages/schemas/source/api/items/runtime.ts b/packages/schemas/source/api/items/runtime.ts new file mode 100644 index 0000000..2c41b54 --- /dev/null +++ b/packages/schemas/source/api/items/runtime.ts @@ -0,0 +1,61 @@ +import { z } from 'zod' + +import { GemSlotType } from '../../enums' +import { Soulbound } from '../../enums/soulbound' +import { apiResponseSchema } from '../../common' + +import { skinSchema } from './common' + +const itemSchema = z.object({ + can_have_attributes: z.boolean().optional(), + category: z.string().optional(), + description: z.string().optional(), + dungeon_item: z.boolean().optional(), + gemstone_slots: z + .array( + z.object({ + costs: z + .array( + z.discriminatedUnion('type', [ + z.object({ + coins: z.number().int().min(1), + type: z.literal('COINS'), + }), + z.object({ + amount: z.number().int().min(1), + item_id: z.string(), + type: z.literal('ITEM'), + }), + ]), + ) + .optional(), + slot_type: z.nativeEnum(GemSlotType), + }), + ) + .optional(), + generator_tier: z.number().int().optional(), + generator: z.string().optional(), + glowing: z.boolean().optional(), + id: z.string(), + material: z.string(), + museum: z.boolean().optional(), + name: z.string(), + npc_sell_price: z.number().optional(), + requirements: z + .array( + z + .object({ + type: z.string(), + }) + .passthrough(), + ) + .optional(), + skin: skinSchema.optional(), + soulbound: z.nativeEnum(Soulbound).optional(), + tier: z.string().optional(), + unstackable: z.boolean().optional(), +}) + +export const itemsResponseSchemaRuntime = apiResponseSchema.extend({ + items: z.array(itemSchema), +}) diff --git a/packages/schemas/source/api/items/strict.ts b/packages/schemas/source/api/items/strict.ts new file mode 100644 index 0000000..fdc0a50 --- /dev/null +++ b/packages/schemas/source/api/items/strict.ts @@ -0,0 +1,538 @@ +import { z } from 'zod' + +import { + CrimsonIsleFaction, + CrystalType, + DungeonType, + EssenceType, + GemSlotType, + ItemCategory, + ItemTier, + MinionType, + PrivateIslandType, + SkillType, + SlayerBossType, +} from '../../enums' +import { Soulbound } from '../../enums/soulbound' +import { UPPER_SNAKE_CASE_REGEX } from '../bazaar/common' +import { apiResponseSchema } from '../../common' + +import { skinSchema } from './common' + +const statsSchemaStrict = z + .preprocess( + (o) => + typeof o === 'object' && o !== null + ? Object.fromEntries( + Object.entries(o).map(([key, value]) => [key.toUpperCase(), value]), + ) + : undefined, + z + .object({ + ABILITY_DAMAGE_PERCENT: z.number().int().optional(), + ATTACK_SPEED: z.number().int().optional(), + BONUS_PEST_CHANCE: z.number().int().optional(), + BREAKING_POWER: z.number().int().optional(), + CARROT_FORTUNE: z.number().int().optional(), + COCOA_BEANS_FORTUNE: z.number().int().optional(), + COLD_RESISTANCE: z.number().int().optional(), + COMBAT_WISDOM: z.number().int().optional(), + CRITICAL_CHANCE: z.number().int().optional(), + CRITICAL_DAMAGE: z.number().int().optional(), + DAMAGE: z.number().int().optional(), + DEFENSE: z.number().int().optional(), + FARMING_FORTUNE: z.number().int().optional(), + FARMING_WISDOM: z.number().int().optional(), + FEROCITY: z.number().int().optional(), + FISHING_SPEED: z.number().int().optional(), + FISHING_WISDOM: z.number().int().optional(), + FORAGING_WISDOM: z.number().int().optional(), + HEALTH_REGENERATION: z.number().int().optional(), + HEALTH: z.number().int().optional(), + INTELLIGENCE: z.number().int().optional(), + MAGIC_FIND: z.number().int().optional(), + MELON_FORTUNE: z.number().int().optional(), + MENDING: z.number().int().optional(), + MINING_FORTUNE: z.number().int().optional(), + MINING_SPEED: z.number().int().optional(), + PET_LUCK: z.number().int().optional(), + POTATO_FORTUNE: z.number().int().optional(), + PUMPKIN_FORTUNE: z.number().int().optional(), + RIFT_DAMAGE: z.number().int().optional(), + RIFT_HEALTH: z.number().int().optional(), + RIFT_INTELLIGENCE: z.number().int().optional(), + RIFT_MANA_REGEN: z.number().int().optional(), + RIFT_TIME: z.number().int().optional(), + RIFT_WALK_SPEED: z.number().int().optional(), + SEA_CREATURE_CHANCE: z.number().optional(), // float + STRENGTH: z.number().int().optional(), + SWING_RANGE: z.number().optional(), // float + TRUE_DEFENSE: z.number().optional(), // float + VITALITY: z.number().int().optional(), + WALK_SPEED: z.number().int().optional(), + WEAPON_ABILITY_DAMAGE: z.number().int().optional(), + WHEAT_FORTUNE: z.number().int().optional(), + }) + .strict(), + ) + .optional() + +export const itemsResponseSchemaStrict = apiResponseSchema + .extend({ + items: z.array( + z + .object({ + ability_damage_scaling: z.number().optional(), // float + can_auction: z.boolean().optional(), + can_burn_in_furnace: z.boolean().optional(), + can_have_attributes: z.boolean().optional(), + can_have_power_scroll: z.boolean().optional(), + can_interact_right_click: z.boolean().optional(), + can_interact_entity: z.boolean().optional(), + can_interact: z.boolean().optional(), + can_place: z.boolean().optional(), + can_recombobulate: z.boolean().optional(), + can_trade: z.boolean().optional(), + has_uuid: z.boolean().optional(), + cannot_reforge: z.boolean().optional(), + catacombs_requirements: z + .array( + z + .object({ + dungeon_type: z.nativeEnum(DungeonType), + level: z.number().int().min(0), + type: z.enum(['DUNGEON_SKILL']), + }) + .strict(), + ) + .optional(), + category: z + .preprocess((value) => { + if (value === 'NONE') return undefined + return value + }, z.nativeEnum(ItemCategory).optional()) + .optional(), + color: z + .string() + .regex(/\d+,\d+,\d+/) + .optional(), + crystal: z.nativeEnum(CrystalType).optional(), + description: z.string().optional(), + double_tap_to_drop: z.boolean().optional(), + dungeon_item: z.boolean().optional(), + dungeon_item_conversion_cost: z + .object({ + amount: z.number().int().min(1), + essence_type: z.nativeEnum(EssenceType), + }) + .strict() + .optional(), + durability: z.number().optional(), + enchantments: z + .object({ + aiming: z.number().int().optional(), // not supposed to exist... + aqua_affinity: z.number().int().optional(), + big_brain: z.number().int().optional(), + counter_strike: z.number().int().optional(), + depth_strider: z.number().int().optional(), + efficiency: z.number().int().optional(), + feather_falling: z.number().int().optional(), + first_strike: z.number().int().optional(), + power: z.number().int().optional(), + quantum: z.number().int().optional(), + rainbow: z.number().int().optional(), + reflection: z.number().int().optional(), + replenish: z.number().int().optional(), + respiration: z.number().int().optional(), + scavenger: z.number().int().optional(), + sharpness: z.number().int().optional(), + telekinesis: z.number().int().optional(), + transylvanian: z.number().int().optional(), + ultimate_the_one: z.number().int().optional(), + vampirism: z.number().int().optional(), + vicious: z.number().int().optional(), + }) + .strict() + .optional(), + furniture: z + .string() + .regex(/[A-Z][A-Z_]+[A-Z]/) + .optional(), + gear_score: z.number().int().optional(), + gemstone_slots: z + .array( + z + .object({ + costs: z + .array( + z.discriminatedUnion('type', [ + z + .object({ + coins: z.number().int().min(1), + type: z.literal('COINS'), + }) + .strict(), + z + .object({ + amount: z.number().int().min(1), + item_id: z.string().regex(UPPER_SNAKE_CASE_REGEX), + type: z.literal('ITEM'), + }) + .strict(), + ]), + ) + .optional(), + slot_type: z.nativeEnum(GemSlotType), + }) + .strict(), + ) + .optional(), + generator_tier: z.number().int().min(1).max(12).optional(), + generator: z.nativeEnum(MinionType).optional(), + glowing: z.boolean().optional(), + hide_from_viewrecipe_command: z.boolean().optional(), + id: z.string(), + item_durability: z.number().int().optional(), + item_specific: z + .object({ + bonus_experience_chance: z.number().int().optional(), + bonus_fishing_speed_per_bucket: z.number().int().optional(), + bonus_fishing_speed: z.number().int().optional(), + bonus_heal: z.number().int().optional(), + bonus_rift_damage_vs_vampire: z.number().int().optional(), + bundled_amount: z.literal(9).optional(), + bundled_item_id: z.literal('ECCENTRIC_PAINTING').optional(), + can_play_snake: z.boolean().optional(), + can_play_tictactoe: z.boolean().optional(), + charges: z.number().int().optional(), + chisel_charges: z.number().int().optional(), + colors: z.array(z.string()).optional(), + consumed_item: z.string().optional(), + cooldown_seconds: z.number().int().optional(), + cycle_back: z.boolean().optional(), + damage_multiplier: z.number().optional(), + damage_per_player: z.number().int().optional(), + duration_seconds: z.number().int().optional(), + duration_ticks: z.number().int().optional(), + effect_duration_seconds: z.number().int().optional(), + experience_gained: z.number().int().optional(), + extra_pelts: z.number().int().min(0).optional(), + flex_skins: z + .array( + z + .object({ + description: z.string(), + name: z.string(), + skin_value: z.string(), + }) + .strict(), + ) + .optional(), + has_contact_directory: z.boolean().optional(), // Abiphone + has_dnd: z.boolean().optional(), // Abiphone + heal: z.number().int().optional(), + heal_on_hit: z.number().optional(), + hearts_reduction: z.number().optional(), + intelligence: z.number().int().optional(), + mana_cost: z.number().int().optional(), + mana_refund: z.number().int().optional(), + mana_regen_per_player: z.number().int().optional(), + max_bonus_fishing_speed: z.number().int().optional(), + max_contacts: z.number().int().optional(), // Abiphone + max_musicdiscs: z.number().int().optional(), // Abiphone + max_players: z.number().int().optional(), + max_other_players: z.number().int().optional(), + memorable_event_key: z + .enum([ + 'community_center_refurbishment', + 'inflation_fixer', + 'pet_care_expansion', + 'repair_wizard_portal', + 'winter_2023', + ]) + .optional(), + motes_on_join_per_eat: z.number().int().optional(), + motes_percent_per_eat: z.number().int().optional(), + permanent_crops_farming_fortune: z.number().int().optional(), + permanent_health: z.number().int().optional(), + piece_offset: z.number().int().optional(), + portal: z + .object({ + description_name: z.string().optional(), + destination_mode: z.string(), + holo_name: z.string(), + location_tag: z.string().optional(), + objective_requirement: z + .object({ + objective_id: z.enum(['go_to_base_camp']), + objective_status: z.enum(['COMPLETE']), + }) + .strict() + .optional(), + offset: z.string().optional(), + schematic_file: z.string(), + skill_requirement: z.object({ + level: z.number().int().min(0), + skill: z.nativeEnum(SkillType), + }), + }) + .strict() + .optional(), + range: z.number().int().optional(), + range_blocks: z.number().int().optional(), + regained_rift_time: z.number().int().optional(), + rift_stats: z + .object({ + rift_damage: z.number().int().optional(), + rift_walk_speed: z.number().int().optional(), + }) + .strict() + .optional(), + rift_time: z.number().int().optional(), + rift_time_gain: z.number().int().optional(), + rift_time_per_eat: z.number().int().optional(), + rift_time_regain_on_kill: z.number().int().optional(), + scaling: z + .object({ + tiers: z.array(z.unknown()).optional(), + }) + .strict() + .optional(), + slow_duration_seconds: z.number().int().optional(), + speed_boost: z.number().int().optional(), + speed_duration_seconds: z.number().int().optional(), + speed_on_farming_island: z.number().int().min(0).optional(), + stats: z + .object({ + attack_speed: z.number().int().optional(), + critical_damage: z.number().int().optional(), + strength: z.number().int().optional(), + walk_speed: z.number().int().optional(), + }) + .strict() + .optional(), + stats_on_rift: z + .object({ + rift_intelligence: z.number().int().optional(), + rift_mana_regen: z.number().int().optional(), + rift_time: z.number().int().optional(), + }) + .strict() + .optional(), + tick_interval: z.number().int().optional(), + tiers: z + .record( + z + .object({ + stats: statsSchemaStrict, + }) + .strict(), + ) + .optional(), + }) + .strict() + .optional(), + lose_motes_value_on_transfer: z.boolean().optional(), + material: z.string().regex(/[A-Z][A-Z_]+[A-Z]/), + motes_sell_price: z.number().int().optional(), + museum: z.boolean().optional(), + name: z.string(), + npc_sell_price: z.number().optional(), + rarity_salvageable: z.boolean().optional(), + prestige: z + .object({ + costs: z.array( + z.union([ + z.object({ + amount: z.number().int().min(1), + essence_type: z.nativeEnum(EssenceType), + }), + z.object({ + amount: z.number().int().min(1), + item_id: z.string().regex(UPPER_SNAKE_CASE_REGEX), + }), + ]), + ), + item_id: z.string().regex(UPPER_SNAKE_CASE_REGEX), + }) + .strict() + .optional(), + private_island: z.nativeEnum(PrivateIslandType).optional(), + origin: z.enum(['BINGO', 'RIFT']).optional(), + rift_transferrable: z.boolean().optional(), + serializable: z.boolean().optional(), + skin: skinSchema.optional(), + soulbound: z.nativeEnum(Soulbound).optional(), + stats: statsSchemaStrict.optional(), + salvage: z + .union([ + z + .object({ + amount: z.number().int().min(1), + essence_type: z.nativeEnum(EssenceType), + }) + .strict(), + z + .object({ + amount: z.number().int().min(1), + item_id: z.string().regex(UPPER_SNAKE_CASE_REGEX), + }) + .strict(), + ]) + .optional(), + salvageable_from_recipe: z.boolean().optional(), + salvages: z + .array( + z.union([ + z + .object({ + amount: z.number().int().min(1), + essence_type: z.nativeEnum(EssenceType), + type: z.literal('ESSENCE'), + }) + .strict(), + z + .object({ + amount: z.number().int().min(1), + item_id: z.string().regex(UPPER_SNAKE_CASE_REGEX), + type: z.literal('ITEM'), + }) + .strict(), + ]), + ) + .optional(), + sword_type: z + .enum(['AXE', 'DAGGER', 'KARAMBIT', 'KATANA', 'SCYTHE']) + .optional(), + tier: z.nativeEnum(ItemTier).optional(), + tiered_stats: z + .object({ + ATTACK_SPEED: z.array(z.number().int()).optional(), + CRITICAL_CHANCE: z.array(z.number().int()).optional(), + CRITICAL_DAMAGE: z.array(z.number().int()).optional(), + DAMAGE: z.array(z.number().int()).optional(), + DEFENSE: z.array(z.number().int()).optional(), + HEALTH: z.array(z.number().int()).optional(), + INTELLIGENCE: z.array(z.number().int()).optional(), + STRENGTH: z.array(z.number().int()).optional(), + WALK_SPEED: z.array(z.number().int()).optional(), + WEAPON_ABILITY_DAMAGE: z.array(z.number().int()).optional(), + }) + .strict() + .optional(), + unstackable: z.boolean().optional(), + upgrade_costs: z + .array( + z.array( + z.union([ + z + .object({ + amount: z.number().int().min(1), + essence_type: z.nativeEnum(EssenceType), + type: z.literal('ESSENCE'), + }) + .strict(), + z.object({ + amount: z.number().int().min(1), + item_id: z.string().regex(UPPER_SNAKE_CASE_REGEX), + }), + ]), + ), + ) + .optional(), + requirements: z + .array( + z.discriminatedUnion('type', [ + z + .object({ + collection: z.enum([ + 'CACTUS', + 'COAL', + 'HARD_STONE', + 'ICE', + 'SAND', + 'SLIME_BALL', + ]), + tier: z.number().int().min(1), + type: z.literal('COLLECTION'), + }) + .strict(), + z + .object({ + faction: z.nativeEnum(CrimsonIsleFaction), + reputation: z.number().int().min(1), + type: z.literal('CRIMSON_ISLE_REPUTATION'), + }) + .strict(), + z + .object({ + dungeon_type: z.nativeEnum(DungeonType), + level: z.number().int().min(1), + type: z.literal('DUNGEON_SKILL'), + }) + .strict(), + z + .object({ + dungeon_type: z.nativeEnum(DungeonType), + tier: z.number().int().min(1), + type: z.literal('DUNGEON_TIER'), + }) + .strict(), + z + .object({ + level: z.number().int().min(1), + type: z.literal('GARDEN_LEVEL'), + }) + .strict(), + z + .object({ + tier: z.number().int().min(1), + type: z.literal('HEART_OF_THE_MOUNTAIN'), + }) + .strict(), + z + .object({ + type: z.literal('MELODY_HAIR'), + }) + .strict(), + z + .object({ + minimum_age: z.number().int(), + minimum_age_unit: z.enum(['DAYS']), + type: z.literal('PROFILE_AGE'), + }) + .strict(), + z + .object({ + level: z.number().int().min(1), + skill: z.nativeEnum(SkillType), + type: z.literal('SKILL'), + }) + .strict(), + z + .object({ + level: z.number().int().min(1), + slayer_boss_type: z.nativeEnum(SlayerBossType), + type: z.literal('SLAYER'), + }) + .strict(), + z + .object({ + mode: z.enum(['I', 'II', 'III']), + type: z.literal('TARGET_PRACTICE'), + }) + .strict(), + z + .object({ + reward: z.enum(['BRONZE', 'SILVER', 'GOLD', 'DIAMOND']), + type: z.literal('TROPHY_FISHING'), + }) + .strict(), + ]), + ) + .optional(), + }) + .strict(), + ), + }) + .strict() diff --git a/packages/schemas/source/api/skills/common.ts b/packages/schemas/source/api/skills/common.ts new file mode 100644 index 0000000..35e241c --- /dev/null +++ b/packages/schemas/source/api/skills/common.ts @@ -0,0 +1,7 @@ +import { z } from 'zod' + +export const levelSchema = z.object({ + level: z.number(), + totalExpRequired: z.number(), + unlocks: z.array(z.string()), +}) diff --git a/packages/schemas/source/api/skills/index.ts b/packages/schemas/source/api/skills/index.ts new file mode 100644 index 0000000..bb1c175 --- /dev/null +++ b/packages/schemas/source/api/skills/index.ts @@ -0,0 +1,2 @@ +export * from './runtime' +export * from './strict' diff --git a/packages/schemas/source/api/skills/runtime.ts b/packages/schemas/source/api/skills/runtime.ts new file mode 100644 index 0000000..b2127c0 --- /dev/null +++ b/packages/schemas/source/api/skills/runtime.ts @@ -0,0 +1,17 @@ +import { z } from 'zod' + +import { apiResponseSchema } from '../../common' + +import { levelSchema } from './common' + +const skillSchema = z.object({ + description: z.string(), + levels: z.array(levelSchema), + maxLevel: z.number(), + name: z.string(), +}) + +export const skillsResponseSchemaRuntime = apiResponseSchema.extend({ + skills: z.record(skillSchema), + version: z.string(), +}) diff --git a/packages/schemas/source/api/skills/strict.ts b/packages/schemas/source/api/skills/strict.ts new file mode 100644 index 0000000..812e2ad --- /dev/null +++ b/packages/schemas/source/api/skills/strict.ts @@ -0,0 +1,35 @@ +import { z } from 'zod' + +import { apiResponseSchema } from '../../common' + +import { levelSchema } from './common' + +const skillSchema = z + .object({ + description: z.string(), + levels: z.array(levelSchema.strict()), + maxLevel: z.number(), + name: z.string(), + }) + .strict() + +export const skillsResponseSchemaStrict = apiResponseSchema + .extend({ + skills: z + .object({ + ALCHEMY: skillSchema, + CARPENTRY: skillSchema, + COMBAT: skillSchema, + ENCHANTING: skillSchema, + FARMING: skillSchema, + FISHING: skillSchema, + FORAGING: skillSchema, + MINING: skillSchema, + RUNECRAFTING: skillSchema, + SOCIAL: skillSchema, + TAMING: skillSchema, + }) + .strict(), + version: z.string().regex(/\d+.\d+.\d+/), + }) + .strict() diff --git a/packages/schemas/source/common/index.ts b/packages/schemas/source/common/index.ts new file mode 100644 index 0000000..482c201 --- /dev/null +++ b/packages/schemas/source/common/index.ts @@ -0,0 +1 @@ +export * from './response' diff --git a/packages/schemas/source/common/response.ts b/packages/schemas/source/common/response.ts new file mode 100644 index 0000000..d5f0627 --- /dev/null +++ b/packages/schemas/source/common/response.ts @@ -0,0 +1,8 @@ +import { z } from 'zod' + +import { toDate } from '../utilities/preprocessors/to-date' + +export const apiResponseSchema = z.object({ + lastUpdated: z.preprocess(toDate, z.date()), + success: z.boolean(), +}) diff --git a/packages/schemas/source/enums/crimson-isle-faction.ts b/packages/schemas/source/enums/crimson-isle-faction.ts new file mode 100644 index 0000000..182c769 --- /dev/null +++ b/packages/schemas/source/enums/crimson-isle-faction.ts @@ -0,0 +1,4 @@ +export enum CrimsonIsleFaction { + BARBARIANS = 'BARBARIANS', + MAGES = 'MAGES', +} diff --git a/packages/schemas/source/enums/dungeons.ts b/packages/schemas/source/enums/dungeons.ts new file mode 100644 index 0000000..313953f --- /dev/null +++ b/packages/schemas/source/enums/dungeons.ts @@ -0,0 +1,15 @@ +export enum DungeonType { + CATACOMBS = 'CATACOMBS', + MASTER_CATACOMBS = 'MASTER_CATACOMBS', +} + +export enum EssenceType { + CRIMSON = 'CRIMSON', + DIAMOND = 'DIAMOND', + DRAGON = 'DRAGON', + GOLD = 'GOLD', + ICE = 'ICE', + SPIDER = 'SPIDER', + UNDEAD = 'UNDEAD', + WITHER = 'WITHER', +} diff --git a/packages/schemas/source/enums/gems.ts b/packages/schemas/source/enums/gems.ts new file mode 100644 index 0000000..3063689 --- /dev/null +++ b/packages/schemas/source/enums/gems.ts @@ -0,0 +1,26 @@ +export enum GemSlotType { + // gemstones + AMBER = 'AMBER', + AMETHYST = 'AMETHYST', + AQUAMARINE = 'AQUAMARINE', + CITRINE = 'CITRINE', + JADE = 'JADE', + JASPER = 'JASPER', + ONYX = 'ONYX', + OPAL = 'OPAL', + PERIDOT = 'PERIDOT', + RUBY = 'RUBY', + SAPPHIRE = 'SAPPHIRE', + TOPAZ = 'TOPAZ', + + // special + /** + * From Mining V3 + */ + CHISEL = 'CHISEL', + COMBAT = 'COMBAT', + DEFENSIVE = 'DEFENSIVE', + MINING = 'MINING', + OFFENSIVE = 'OFFENSIVE', + UNIVERSAL = 'UNIVERSAL', +} diff --git a/packages/schemas/source/enums/index.ts b/packages/schemas/source/enums/index.ts new file mode 100644 index 0000000..15ba178 --- /dev/null +++ b/packages/schemas/source/enums/index.ts @@ -0,0 +1,9 @@ +export * from './crimson-isle-faction' +export * from './dungeons' +export * from './gems' +export * from './items' +export * from './minions' +export * from './other' +export * from './skills' +export * from './slayers' +export * from './soulbound' diff --git a/packages/schemas/source/enums/items.ts b/packages/schemas/source/enums/items.ts new file mode 100644 index 0000000..6664bc2 --- /dev/null +++ b/packages/schemas/source/enums/items.ts @@ -0,0 +1,50 @@ +export enum ItemCategory { + ACCESSORY = 'ACCESSORY', + ARROW = 'ARROW', + ARROW_POISON = 'ARROW_POISON', + AXE = 'AXE', + BAIT = 'BAIT', + BELT = 'BELT', + BOOTS = 'BOOTS', + BOW = 'BOW', + BRACELET = 'BRACELET', + CHESTPLATE = 'CHESTPLATE', + CHISEL = 'CHISEL', + CLOAK = 'CLOAK', + COSMETIC = 'COSMETIC', + DEPLOYABLE = 'DEPLOYABLE', + DRILL = 'DRILL', + DUNGEON_PASS = 'DUNGEON_PASS', + FISHING_ROD = 'FISHING_ROD', + FISHING_WEAPON = 'FISHING_WEAPON', + GAUNTLET = 'GAUNTLET', + GLOVES = 'GLOVES', + HELMET = 'HELMET', + HOE = 'HOE', + LEGGINGS = 'LEGGINGS', + LONGSWORD = 'LONGSWORD', + MEMENTO = 'MEMENTO', + NECKLACE = 'NECKLACE', + PET_ITEM = 'PET_ITEM', + PICKAXE = 'PICKAXE', + PORTAL = 'PORTAL', + REFORGE_STONE = 'REFORGE_STONE', + SHEARS = 'SHEARS', + SPADE = 'SPADE', + SWORD = 'SWORD', + TRAVEL_SCROLL = 'TRAVEL_SCROLL', + VACUUM = 'VACUUM', + WAND = 'WAND', +} + +export enum ItemTier { + COMMON = 'COMMON', + EPIC = 'EPIC', + LEGENDARY = 'LEGENDARY', + MYTHIC = 'MYTHIC', + RARE = 'RARE', + SPECIAL = 'SPECIAL', + UNCOMMON = 'UNCOMMON', + UNOBTAINABLE = 'UNOBTAINABLE', + VERY_SPECIAL = 'VERY_SPECIAL', +} diff --git a/packages/schemas/source/enums/minions.ts b/packages/schemas/source/enums/minions.ts new file mode 100644 index 0000000..4ea5407 --- /dev/null +++ b/packages/schemas/source/enums/minions.ts @@ -0,0 +1,61 @@ +export enum MinionType { + ACACIA = 'ACACIA', + BIRCH = 'BIRCH', + BLAZE = 'BLAZE', + CACTUS = 'CACTUS', + CARROT = 'CARROT', + CAVESPIDER = 'CAVESPIDER', + CHICKEN = 'CHICKEN', + CLAY = 'CLAY', + COAL = 'COAL', + COBBLESTONE = 'COBBLESTONE', + COCOA = 'COCOA', + COW = 'COW', + CREEPER = 'CREEPER', + DARK_OAK = 'DARK_OAK', + DIAMOND = 'DIAMOND', + EMERALD = 'EMERALD', + ENDER_STONE = 'ENDER_STONE', + ENDERMAN = 'ENDERMAN', + FISHING = 'FISHING', + FLOWER = 'FLOWER', + GHAST = 'GHAST', + GLOWSTONE = 'GLOWSTONE', + GOLD = 'GOLD', + GRAVEL = 'GRAVEL', + HARD_STONE = 'HARD_STONE', + ICE = 'ICE', + INFERNO = 'INFERNO', + IRON = 'IRON', + JUNGLE = 'JUNGLE', + LAPIS = 'LAPIS', + MAGMA_CUBE = 'MAGMA_CUBE', + MELON = 'MELON', + MITHRIL = 'MITHRIL', + MUSHROOM = 'MUSHROOM', + MYCELIUM = 'MYCELIUM', + NETHER_WARTS = 'NETHER_WARTS', + OAK = 'OAK', + OBSIDIAN = 'OBSIDIAN', + PIG = 'PIG', + POTATO = 'POTATO', + PUMPKIN = 'PUMPKIN', + QUARTZ = 'QUARTZ', + RABBIT = 'RABBIT', + RED_SAND = 'RED_SAND', + REDSTONE = 'REDSTONE', + REVENANT = 'REVENANT', + SAND = 'SAND', + SHEEP = 'SHEEP', + SKELETON = 'SKELETON', + SLIME = 'SLIME', + SNOW = 'SNOW', + SPIDER = 'SPIDER', + SPRUCE = 'SPRUCE', + SUGAR_CANE = 'SUGAR_CANE', + TARANTULA = 'TARANTULA', + VAMPIRE = 'VAMPIRE', + VOIDLING = 'VOIDLING', + WHEAT = 'WHEAT', + ZOMBIE = 'ZOMBIE', +} diff --git a/packages/schemas/source/enums/other.ts b/packages/schemas/source/enums/other.ts new file mode 100644 index 0000000..ce4db07 --- /dev/null +++ b/packages/schemas/source/enums/other.ts @@ -0,0 +1,24 @@ +export enum PrivateIslandType { + BARN = 'BARN', + DESERT = 'DESERT', + FARMING = 'FARMING', + MINING = 'MINING', + MINING_FOREST = 'MINING_FOREST', + NETHER = 'NETHER', + NETHER_WART = 'NETHER_WART', + POND = 'POND', + WINTER = 'WINTER', +} + +export enum CrystalType { + DESERT_ISLAND = 'DESERT_ISLAND', + FARM = 'FARM', + FISHING = 'FISHING', + FOREST_ISLAND = 'FOREST_ISLAND', + MITHRIL = 'MITHRIL', + NETHER_WART_ISLAND = 'NETHER_WART_ISLAND', + RESOURCE_REGENERATOR = 'RESOURCE_REGENERATOR', + WHEAT_ISLAND = 'WHEAT_ISLAND', + WINTER_ISLAND = 'WINTER_ISLAND', + WOODCUTTING = 'WOODCUTTING', +} diff --git a/packages/schemas/source/enums/skills.ts b/packages/schemas/source/enums/skills.ts new file mode 100644 index 0000000..baa8134 --- /dev/null +++ b/packages/schemas/source/enums/skills.ts @@ -0,0 +1,9 @@ +export enum SkillType { + COMBAT = 'COMBAT', + ENCHANTING = 'ENCHANTING', + FARMING = 'FARMING', + FISHING = 'FISHING', + FORAGING = 'FORAGING', + MINING = 'MINING', + SOCIAL = 'SOCIAL', +} diff --git a/packages/schemas/source/enums/slayers.ts b/packages/schemas/source/enums/slayers.ts new file mode 100644 index 0000000..5278da1 --- /dev/null +++ b/packages/schemas/source/enums/slayers.ts @@ -0,0 +1,7 @@ +export enum SlayerBossType { + BLAZE = 'blaze', + ENDERMAN = 'enderman', + SPIDER = 'spider', + WOLF = 'wolf', + ZOMBIE = 'zombie', +} diff --git a/packages/schemas/source/enums/soulbound.ts b/packages/schemas/source/enums/soulbound.ts new file mode 100644 index 0000000..97f0fa2 --- /dev/null +++ b/packages/schemas/source/enums/soulbound.ts @@ -0,0 +1,4 @@ +export enum Soulbound { + COOP = 'COOP', + SOLO = 'SOLO', +} diff --git a/packages/schemas/source/index.ts b/packages/schemas/source/index.ts new file mode 100644 index 0000000..8fda396 --- /dev/null +++ b/packages/schemas/source/index.ts @@ -0,0 +1,4 @@ +export * as api from './api' +export * as common from './common' +export * as enums from './enums' +export * as utilities from './utilities' diff --git a/packages/schemas/source/utilities/index.ts b/packages/schemas/source/utilities/index.ts new file mode 100644 index 0000000..2842ec5 --- /dev/null +++ b/packages/schemas/source/utilities/index.ts @@ -0,0 +1,4 @@ +import * as preprocessors from './preprocessors' +import * as transforms from './transforms' + +export { preprocessors, transforms } diff --git a/packages/schemas/source/utilities/preprocessors/base64-json-to-object.ts b/packages/schemas/source/utilities/preprocessors/base64-json-to-object.ts new file mode 100644 index 0000000..9d1cff2 --- /dev/null +++ b/packages/schemas/source/utilities/preprocessors/base64-json-to-object.ts @@ -0,0 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const base64JsonToObject = (base64: unknown): any => { + if (typeof base64 === 'string') + return JSON.parse(Buffer.from(base64, 'base64').toString()) +} diff --git a/packages/schemas/source/utilities/preprocessors/index.ts b/packages/schemas/source/utilities/preprocessors/index.ts new file mode 100644 index 0000000..c3ea909 --- /dev/null +++ b/packages/schemas/source/utilities/preprocessors/index.ts @@ -0,0 +1,2 @@ +export * from './base64-json-to-object' +export * from './to-date' diff --git a/packages/schemas/source/utilities/preprocessors/to-date.ts b/packages/schemas/source/utilities/preprocessors/to-date.ts new file mode 100644 index 0000000..a0ca129 --- /dev/null +++ b/packages/schemas/source/utilities/preprocessors/to-date.ts @@ -0,0 +1,3 @@ +export const toDate = (timestamp: unknown) => { + if (typeof timestamp === 'number') return new Date(timestamp) +} diff --git a/packages/schemas/source/utilities/transforms/index.ts b/packages/schemas/source/utilities/transforms/index.ts new file mode 100644 index 0000000..0551997 --- /dev/null +++ b/packages/schemas/source/utilities/transforms/index.ts @@ -0,0 +1 @@ +export * from './url-transform-https' diff --git a/packages/schemas/source/utilities/transforms/url-transform-https.ts b/packages/schemas/source/utilities/transforms/url-transform-https.ts new file mode 100644 index 0000000..53599bd --- /dev/null +++ b/packages/schemas/source/utilities/transforms/url-transform-https.ts @@ -0,0 +1,6 @@ +import { z } from 'zod' + +export const urlTransformHttps = z.preprocess( + (url) => (url as string).replace(/^http:/, 'https:'), + z.string().url(), +) diff --git a/packages/schemas/tsconfig.json b/packages/schemas/tsconfig.json new file mode 100644 index 0000000..677ab7a --- /dev/null +++ b/packages/schemas/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "module": "commonjs", + "noImplicitAny": true, + "noUnusedLocals": true, + "outDir": "./dist", + "resolveJsonModule": true, + "rootDir": "./source", + "strict": true, + "strictNullChecks": true, + "target": "es5", + "tsBuildInfoFile": "./dist/.tsbuildinfo", + "types": ["node"] + }, + "exclude": ["source/**/*.test.ts"], + "include": ["source/**/*.ts"] +} diff --git a/packages/schemas/turbo.json b/packages/schemas/turbo.json new file mode 100644 index 0000000..6b706fd --- /dev/null +++ b/packages/schemas/turbo.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "pipeline": { + "build": { + "inputs": ["./source/**", "tsconfig.json"], + "outputMode": "new-only", + "outputs": ["./dist/**"] + }, + "download": { + "dependsOn": ["^build"], + "inputs": ["./scripts/**"], + "outputMode": "new-only", + "outputs": ["./data"] + }, + "start": { + "dependsOn": ["^build", "download"] + }, + "watch": { + "cache": false, + "dependsOn": ["^build", "download"], + "persistent": true + } + } +} diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 0000000..609d8a7 --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1,10 @@ +export default { + arrowParens: 'always', + endOfLine: 'lf', + quoteProps: 'as-needed', + semi: false, + singleQuote: true, + tabWidth: 2, + trailingComma: 'all', + useTabs: true, +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..be87e01 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "ESNext", + "moduleDetection": "force", + "moduleResolution": "bundler", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "verbatimModuleSyntax": true + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..8bea8ba --- /dev/null +++ b/turbo.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://turbo.build/schema.json", + "globalDependencies": ["bun.lockb"], + "pipeline": { + "build": { + "dependsOn": ["^build"] + }, + "check": { + "dependsOn": [ + "check:eslint", + "check:knip", + "check:prettier", + "check:publint" + ] + }, + "check:eslint": { + "dependsOn": ["@skyblock-finance/eslint-config#build"], + "inputs": [ + "**/*.{cjs,js,mjs,json,mjs,mts,ts}", + "../../eslint.config.mjs" + ], + "outputMode": "new-only" + }, + "check:knip": { + "outputMode": "new-only" + }, + "check:prettier": { + "inputs": ["**/*.{css,js,json,md,scss,ts}"], + "outputMode": "new-only" + }, + "check:publint": { + "dependsOn": ["build"], + "inputs": ["dist/**", "package.json"], + "outputMode": "new-only" + }, + "fix": { + "dependsOn": ["fix:eslint", "fix:prettier"] + }, + "fix:eslint": { + "dependsOn": ["@skyblock-finance/eslint-config#build"], + "inputs": [ + "**/*.{cjs,js,mjs,json,mjs,mts,ts}", + "../../eslint.config.mjs" + ], + "outputMode": "new-only", + "outputs": [ + "**/*.{cjs,js,mjs,json,mjs,mts,ts}", + "../../eslint.config.mjs" + ] + }, + "fix:prettier": { + "inputs": ["**/*.{css,js,json,md,scss,ts}"], + "outputMode": "new-only" + } + } +}