diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d35bbf7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d420c7 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# hive-tools-database + +Built with Postgres and DrizzleORM, with zod for type-safety. \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..8bb55a8 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,3 @@ +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; + +export default [eslintPluginPrettierRecommended]; diff --git a/package.json b/package.json new file mode 100644 index 0000000..9e45485 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "hive-tools-database", + "version": "1.0.0", + "module": "src/index.ts", + "type": "module", + "author": "paroxity", + "dependencies": { + "drizzle-kit": "^0.21.4", + "drizzle-orm": "^0.30.10", + "drizzle-zod": "^0.5.1", + "postgres": "^3.4.4", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^20.12.12", + "eslint": "^9.3.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.5", + "typescript": "^5.4.5" + } +} diff --git a/prettier.config.ts b/prettier.config.ts new file mode 100644 index 0000000..c3daa57 --- /dev/null +++ b/prettier.config.ts @@ -0,0 +1,16 @@ +import type { Config } from "prettier"; + +const config: Config = { + printWidth: 80, + useTabs: true, + semi: true, + singleQuote: false, + quoteProps: "as-needed", + jsxSingleQuote: false, + trailingComma: "none", + bracketSpacing: true, + arrowParens: "avoid", + endOfLine: "lf", +}; + +export default config; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..cc63e50 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,20 @@ +import postgres from "postgres"; +import { drizzle } from "drizzle-orm/postgres-js"; +import { cosmetics, players } from "./schema.ts"; + +/** + * @param connectionString See {@link https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS Postgres Connection URIs} + */ +export default function initializeDatabase({ + connectionUri, +}: { + connectionUri: string; +}) { + const client = postgres(connectionUri, { prepare: false }); + const db = drizzle(client, { schema: { cosmetics, players } }); + + return { client, db }; +} + +export * from "./types.ts"; +export { CosmeticSchema, PlayerSchema } from "./schema.ts"; diff --git a/src/schema.ts b/src/schema.ts new file mode 100644 index 0000000..d826dee --- /dev/null +++ b/src/schema.ts @@ -0,0 +1,78 @@ +import { + integer, + json, + pgTable, + real, + text, + timestamp, +} from "drizzle-orm/pg-core"; +import { + CosmeticImage, + CosmeticImageLocalizedString, + CosmeticRating, + PlayerAvatar, +} from "./types.ts"; +import { sql } from "drizzle-orm"; +import { z } from "zod"; + +/** + * Players Schema + */ + +export const players = pgTable("players", { + id: text("uuid").primaryKey().notNull(), + username: text("username").notNull(), + rank: text("rank").notNull(), + equipped_avatar: json("equipped_avatar"), +}); + +export const PlayerSchema = z.object({ + id: z.string(), + username: z.string(), + rank: z.string(), + equipped_avatar: PlayerAvatar, +}); +export type PlayerSchema = z.infer; + +/** + * Cosmetics Schema + */ + +export const cosmetics = pgTable("cosmetics", { + id: text("id").primaryKey().notNull(), + title: json("title").notNull(), + description: json("description").notNull(), + creation_date: timestamp("creation_date").notNull(), + last_modified: timestamp("last_modified").notNull(), + start_date: timestamp("start_date"), + end_date: timestamp("end_date"), + thumbnail: text("thumbnail").notNull(), + images: json("images").notNull(), + avg_rating: real("avg_rating").notNull(), + ratings: json("ratings").notNull(), + price: integer("price").notNull(), + manual_tags: text("manual_tags") + .array() + .default(sql`ARRAY[]::text[]`), + row_created: timestamp("row_created").defaultNow(), + row_updated: timestamp("row_updated").defaultNow(), +}); + +export const CosmeticSchema = z.object({ + id: z.string(), + title: CosmeticImageLocalizedString, + description: CosmeticImageLocalizedString, + creation_date: z.date(), + last_modified: z.date(), + start_date: z.date().nullable(), + end_date: z.date().nullable(), + thumbnail: z.string(), + images: z.array(CosmeticImage), + avg_rating: z.number(), + ratings: CosmeticRating, + price: z.number(), + manual_tags: z.array(z.string()), + row_created: z.date().optional(), + row_updated: z.date().optional(), +}); +export type CosmeticSchema = z.infer; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..7f7735b --- /dev/null +++ b/src/types.ts @@ -0,0 +1,45 @@ +import { z } from "zod"; + +/** + * Player Types + */ + +export const PlayerAvatar = z.object({ + url: z.string(), + name: z.string(), +}); +export type PlayerAvatar = z.infer; + +/** + * Cosmetic Types + */ + +export const CosmeticImageLocalizedString = z + .object({ + neutral: z.string(), + }) + .catchall(z.string()); +export type CosmeticImageLocalizedString = z.infer< + typeof CosmeticImageLocalizedString +>; + +export const CosmeticImage = z.object({ + Id: z.string(), + Tag: z.string(), + Type: z.string(), + Url: z.string(), +}); +export type CosmeticImage = z.infer; + +const CosmeticRatingDefaultNumber = z.number().default(0); + +export const CosmeticRating = z.object({ + Average: CosmeticRatingDefaultNumber, + TotalCount: CosmeticRatingDefaultNumber, + Count5Star: CosmeticRatingDefaultNumber, + Count4Star: CosmeticRatingDefaultNumber, + Count3Star: CosmeticRatingDefaultNumber, + Count2Star: CosmeticRatingDefaultNumber, + Count1Star: CosmeticRatingDefaultNumber, +}); +export type CosmeticRating = z.infer; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6b82e99 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + "sourceMap": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}