Skip to content

Commit ae87807

Browse files
[chore πŸš“] schema defined with types πŸ±β€πŸ
1 parent 280262b commit ae87807

19 files changed

Lines changed: 186 additions & 129 deletions

File tree

β€Žpackage.jsonβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"react-dom": "^19.0.0",
3232
"sonner": "^2.0.6",
3333
"tailwind-merge": "^3.3.1",
34-
"tailwindcss-animate": "^1.0.7"
34+
"tailwindcss-animate": "^1.0.7",
35+
"zod": "^4.0.14"
3536
},
3637
"devDependencies": {
3738
"@eslint/eslintrc": "^3",

β€Žpnpm-lock.yamlβ€Ž

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

β€Žserver/package.jsonβ€Ž

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "",
55
"author": "",
66
"private": true,
7-
"license": "UNLICENSED",
7+
"license": "MIT",
88
"scripts": {
99
"build": "nest build",
1010
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
@@ -20,8 +20,7 @@
2020
"test:e2e": "jest --config ./test/jest-e2e.json",
2121
"seed": "tsx src/drizzle/seed.ts",
2222
"clean": "rm -rf src/drizzle/out && tsx src/drizzle/clean.ts",
23-
"migrate": "drizzle-kit generate && cross-env DB_MIGRATING=true tsx src/drizzle/migrate.ts",
24-
"generate": "drizzle-kit generate"
23+
"migrate": "drizzle-kit generate && cross-env DB_MIGRATING=true tsx src/drizzle/migrate.ts"
2524
},
2625
"dependencies": {
2726
"@nestjs/common": "^11.0.1",
Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,42 @@
11
import { z } from "zod";
2+
import {
3+
zId,
4+
zOptionalId,
5+
zDecimalString,
6+
zEnum,
7+
zSlug,
8+
zSemver,
9+
zOptionalUrl,
10+
zStringArray,
11+
} from "@/utils";
212
import { browsers, extensionStatus } from "../extensions.schema";
313

414
export const extensionSchema = z.object({
5-
name: z.string().min(1),
6-
slug: z.string().optional(),
15+
sellerId: zId("Seller ID"),
16+
categoryId: zOptionalId("Category ID"),
717

8-
sellerId: z.number(),
9-
categoryId: z.number(),
18+
name: z.string().min(3, "Name must be at least 3 characters"),
19+
slug: zSlug(),
1020

11-
description: z.string().min(1),
12-
shortDescription: z.string().optional(),
21+
description: z.string().min(10, "Description must be at least 10 characters"),
22+
shortDescription: z.string().max(255).optional(),
1323

14-
price: z.string().regex(/^\d{1,8}(\.\d{1,2})?$/, "Invalid price format"),
15-
version: z.string().min(1),
24+
price: zDecimalString(2),
25+
originalPrice: zDecimalString(2),
1626

17-
browsers: z.array(z.enum(browsers)).default(["chrome"]),
18-
tags: z.array(z.string()).optional(),
19-
iconUrl: z.url().optional(),
20-
screenshots: z.array(z.string()).optional(),
21-
videoUrl: z.url().optional(),
22-
downloadUrl: z.url().optional(),
27+
version: zSemver(),
28+
browsers: z.array(zEnum(browsers, "Browser")).default([]),
29+
tags: zStringArray(),
30+
screenshots: zStringArray(),
2331

24-
downloadCount: z.number().int().nonnegative().default(0),
25-
rating: z
26-
.string()
27-
.regex(/^\d{1}(\.\d{1,2})?$/, "Invalid rating format")
28-
.default("0.00"),
32+
iconUrl: zOptionalUrl(),
33+
videoUrl: zOptionalUrl(),
34+
downloadUrl: zOptionalUrl().optional(),
2935

30-
reviewCount: z.number().int().nonnegative().optional(),
31-
status: z.enum(extensionStatus).optional(),
32-
});
36+
downloadCount: z.number().int().min(0).default(0),
37+
rating: zDecimalString(2).default("0.00"),
38+
reviewCount: z.number().int().min(0).default(0),
3339

40+
status: zEnum(extensionStatus, "Status").default("draft"),
41+
});
3442
export type ExtensionSchema = z.infer<typeof extensionSchema>;

β€Žserver/src/modules/extensions/extension.repository.tsβ€Ž

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ import extensions from "./extensions.schema";
77
@Injectable()
88
export class ExtensionRepository {
99
async create(input: ExtensionSchema) {
10-
const [extension] = await db
11-
.insert(extensions)
12-
.values({ ...input, slug: String(input.slug) })
13-
.returning();
10+
const [extension] = await db.insert(extensions).values(input).returning();
1411
return extension;
1512
}
1613
async fetch() {

β€Žserver/src/modules/extensions/extensions.schema.tsβ€Ž

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,13 @@ const extensions = pgTable(
4646

4747
description: text("description").notNull(),
4848
shortDescription: text("short_description"),
49+
4950
price: decimal("price", { precision: 10, scale: 2 }).notNull(),
51+
originalPrice: decimal("original_price", {
52+
precision: 10,
53+
scale: 2,
54+
}).notNull(),
55+
5056
version: text("version").notNull(),
5157
browsers: json("browsers").$type<(typeof browsers)[number][]>().default([]),
5258
tags: json("tags").$type<string[]>().default([]),

β€Žserver/src/utils/db-utility/base.schema.tsβ€Ž

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { sql } from "drizzle-orm";
22
import { serial, timestamp } from "drizzle-orm/pg-core";
33

44
export const baseSchema = {
5-
// id: primaryId(),
65
id: serial("id").primaryKey(),
76

87
createdAt: timestamp("createdAt", {
@@ -15,6 +14,4 @@ export const baseSchema = {
1514
})
1615
.$defaultFn(() => sql`NULL`)
1716
.$onUpdateFn(() => new Date()),
18-
// createdAt: createdAt(),
19-
// updatedAt: updatedAt(),
2017
};

β€Žserver/src/utils/index.tsβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./zod-utils";
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { z } from "zod";
2+
3+
/* Generic decimal validator (as string) */
4+
export const zDecimalString = (precision = 2) =>
5+
z.string().regex(new RegExp(`^\\d+(\\.\\d{1,${precision}})?$`), {
6+
message: `Must be a valid decimal with up to ${precision} decimal places`,
7+
});
8+
9+
// Generic integer ID
10+
export const zId = (field = "ID") =>
11+
z
12+
.number({ error: `${field} is required` })
13+
.int(`${field} must be an integer`)
14+
.positive(`${field} must be a positive number`);
15+
16+
// Optional ID (used for nullable foreign keys)
17+
export const zOptionalId = (field = "ID") =>
18+
z.number().int(`${field} must be an integer`).positive().optional();
19+
20+
// Generic semantic version string
21+
export const zSemver = () =>
22+
z.string().regex(/^\d+(\.\d+){0,2}$/, {
23+
message: "Version must follow semantic versioning (e.g. 1.0.0)",
24+
});
25+
26+
// Generic slug
27+
export const zSlug = () =>
28+
z
29+
.string()
30+
.min(3, "Slug must be at least 3 characters")
31+
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
32+
message: "Slug must be lowercase and hyphen-separated",
33+
});
34+
35+
// Generic optional URL
36+
export const zOptionalUrl = () => z.url("Must be a valid URL").optional();
37+
38+
// Generic enum (based on const array)
39+
export const zEnum = <T extends readonly string[]>(
40+
values: T,
41+
label = "Value",
42+
) =>
43+
z.enum(values, {
44+
error: `${label} must be one of: ${values.join(", ")}`,
45+
});
46+
47+
// Generic string array
48+
export const zStringArray = () =>
49+
z.array(z.string().min(1, "Empty strings are not allowed")).default([]);
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
export const browser = [
1+
export const browsers = [
22
"chrome",
33
"firefox",
4-
"microsoft-edge",
5-
"safary",
6-
"all",
4+
"microsoft edge",
5+
"safari",
76
] as const;
8-
export type Browser = (typeof browser)[number];
7+
export type Browser = (typeof browsers)[number];

0 commit comments

Comments
Β (0)