chore: bootstrap monorepo workspace and shared schemas
This commit is contained in:
40
.env.example
Normal file
40
.env.example
Normal file
@@ -0,0 +1,40 @@
|
||||
# ==========================================
|
||||
# Database
|
||||
# ==========================================
|
||||
POSTGRES_USER=wishlist
|
||||
POSTGRES_PASSWORD=change_me
|
||||
POSTGRES_DB=family_wishlist
|
||||
# DATABASE_URL uses the docker-compose service name `postgres`.
|
||||
# When running backend outside docker against docker-postgres use localhost:5432.
|
||||
DATABASE_URL=postgresql://wishlist:change_me@postgres:5432/family_wishlist
|
||||
|
||||
# ==========================================
|
||||
# Users (two fixed accounts)
|
||||
# Generate hashes with:
|
||||
# pnpm hash-password "YourPlainPassword"
|
||||
# The plain password is NEVER stored anywhere in the app.
|
||||
# ==========================================
|
||||
USER1_USERNAME=alice
|
||||
USER1_PASSWORD_HASH=
|
||||
USER1_SLUG=alice
|
||||
USER1_DISPLAY_NAME=Alice
|
||||
|
||||
USER2_USERNAME=bob
|
||||
USER2_PASSWORD_HASH=
|
||||
USER2_SLUG=bob
|
||||
USER2_DISPLAY_NAME=Bob
|
||||
|
||||
# ==========================================
|
||||
# Secrets (generate: openssl rand -hex 32)
|
||||
# ==========================================
|
||||
JWT_SECRET=
|
||||
COOKIE_SECRET=
|
||||
|
||||
# ==========================================
|
||||
# Runtime
|
||||
# ==========================================
|
||||
NODE_ENV=development
|
||||
BACKEND_PORT=3000
|
||||
PUBLIC_APP_URL=http://localhost:8080
|
||||
UPLOADS_DIR=./uploads
|
||||
LOG_LEVEL=info
|
||||
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# dependencies
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
|
||||
# build artifacts
|
||||
dist/
|
||||
build/
|
||||
.turbo/
|
||||
*.tsbuildinfo
|
||||
|
||||
# env / secrets
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# uploads (dev)
|
||||
apps/backend/uploads/
|
||||
!apps/backend/uploads/.gitkeep
|
||||
|
||||
# prisma
|
||||
apps/backend/prisma/dev.db
|
||||
apps/backend/prisma/migrations/dev/
|
||||
|
||||
# logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# editor / os
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.example.json
|
||||
7
.prettierignore
Normal file
7
.prettierignore
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
dist
|
||||
build
|
||||
.pnpm-store
|
||||
apps/backend/prisma/migrations
|
||||
apps/backend/uploads
|
||||
pnpm-lock.yaml
|
||||
9
.prettierrc.json
Normal file
9
.prettierrc.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "family-wishlist",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"description": "Family Wishlist monorepo: Fastify + Prisma backend, React + Vite frontend",
|
||||
"packageManager": "pnpm@9.12.0",
|
||||
"engines": {
|
||||
"node": ">=20.10.0",
|
||||
"pnpm": ">=9"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "pnpm -r --parallel --stream run dev",
|
||||
"build": "pnpm -r run build",
|
||||
"typecheck": "pnpm -r run typecheck",
|
||||
"lint": "pnpm -r run lint",
|
||||
"format": "prettier --write .",
|
||||
"hash-password": "pnpm --filter @family-wishlist/backend hash-password"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
23
packages/shared/package.json
Normal file
23
packages/shared/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@family-wishlist/shared",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "echo 'shared is source-only'",
|
||||
"lint": "echo 'skip'",
|
||||
"dev": "echo 'shared is source-only'"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
17
packages/shared/src/auth.schema.ts
Normal file
17
packages/shared/src/auth.schema.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const loginSchema = z.object({
|
||||
username: z.string().trim().min(3).max(64),
|
||||
password: z.string().min(8).max(200),
|
||||
});
|
||||
|
||||
export type LoginInput = z.infer<typeof loginSchema>;
|
||||
|
||||
export const authUserSchema = z.object({
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
slug: z.string(),
|
||||
displayName: z.string(),
|
||||
});
|
||||
|
||||
export type AuthUser = z.infer<typeof authUserSchema>;
|
||||
5
packages/shared/src/index.ts
Normal file
5
packages/shared/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './auth.schema.js';
|
||||
export * from './profile.schema.js';
|
||||
export * from './wish.schema.js';
|
||||
export * from './public.schema.js';
|
||||
export * from './types.js';
|
||||
30
packages/shared/src/profile.schema.ts
Normal file
30
packages/shared/src/profile.schema.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const slugRegex = /^[a-z0-9](?:[a-z0-9-]{1,30}[a-z0-9])?$/;
|
||||
|
||||
export const profileSchema = z.object({
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
slug: z.string(),
|
||||
displayName: z.string(),
|
||||
bio: z.string().nullable(),
|
||||
avatarUrl: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
});
|
||||
|
||||
export type Profile = z.infer<typeof profileSchema>;
|
||||
|
||||
export const updateProfileSchema = z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.regex(slugRegex, 'Slug must be 3-32 chars, lowercase letters, digits, hyphens')
|
||||
.optional(),
|
||||
displayName: z.string().trim().min(1).max(64).optional(),
|
||||
bio: z.string().trim().max(500).nullable().optional(),
|
||||
avatarUrl: z.string().url().nullable().optional(),
|
||||
});
|
||||
|
||||
export type UpdateProfileInput = z.infer<typeof updateProfileSchema>;
|
||||
16
packages/shared/src/public.schema.ts
Normal file
16
packages/shared/src/public.schema.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const publicProfileSchema = z.object({
|
||||
slug: z.string(),
|
||||
displayName: z.string(),
|
||||
bio: z.string().nullable(),
|
||||
avatarUrl: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type PublicProfile = z.infer<typeof publicProfileSchema>;
|
||||
|
||||
export const markSeenSchema = z.object({
|
||||
wishIds: z.array(z.string().cuid2().or(z.string())).min(1).max(500),
|
||||
});
|
||||
|
||||
export type MarkSeenInput = z.infer<typeof markSeenSchema>;
|
||||
9
packages/shared/src/types.ts
Normal file
9
packages/shared/src/types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface ApiError {
|
||||
error: string;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
export interface VersionInfo {
|
||||
backend: string;
|
||||
}
|
||||
61
packages/shared/src/wish.schema.ts
Normal file
61
packages/shared/src/wish.schema.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const wishStatus = z.enum(['ACTIVE', 'ARCHIVED', 'COMPLETED', 'DELETED']);
|
||||
export type WishStatus = z.infer<typeof wishStatus>;
|
||||
|
||||
export const imageSource = z.enum(['DEFAULT', 'OG', 'UPLOADED']);
|
||||
export type ImageSource = z.infer<typeof imageSource>;
|
||||
|
||||
export const wishSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
price: z.string().nullable(),
|
||||
currency: z.string(),
|
||||
url: z.string().nullable(),
|
||||
comment: z.string().nullable(),
|
||||
imageUrl: z.string().nullable(),
|
||||
imageSource: imageSource,
|
||||
status: wishStatus,
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
archivedAt: z.string().nullable(),
|
||||
completedAt: z.string().nullable(),
|
||||
deletedAt: z.string().nullable(),
|
||||
sourceWishId: z.string().nullable(),
|
||||
isNewForGuest: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type Wish = z.infer<typeof wishSchema>;
|
||||
|
||||
export const createWishSchema = z.object({
|
||||
title: z.string().trim().min(1).max(200),
|
||||
price: z
|
||||
.union([
|
||||
z.string().trim().regex(/^\d+(\.\d{1,2})?$/, 'Price must be a number with up to 2 decimals'),
|
||||
z.literal(''),
|
||||
z.null(),
|
||||
])
|
||||
.optional()
|
||||
.transform((v) => (v === '' || v == null ? null : v)),
|
||||
currency: z.string().trim().length(3).toUpperCase().default('RUB'),
|
||||
url: z
|
||||
.union([z.string().trim().url(), z.literal(''), z.null()])
|
||||
.optional()
|
||||
.transform((v) => (v === '' || v == null ? null : v)),
|
||||
comment: z
|
||||
.union([z.string().trim().max(2000), z.literal(''), z.null()])
|
||||
.optional()
|
||||
.transform((v) => (v === '' || v == null ? null : v)),
|
||||
});
|
||||
|
||||
export type CreateWishInput = z.infer<typeof createWishSchema>;
|
||||
|
||||
export const updateWishSchema = createWishSchema.partial();
|
||||
export type UpdateWishInput = z.infer<typeof updateWishSchema>;
|
||||
|
||||
export const wishStatusQuery = z
|
||||
.enum(['active', 'archived', 'completed', 'deleted'])
|
||||
.default('active');
|
||||
|
||||
export const NEW_BADGE_DAYS = 5;
|
||||
export const TRASH_RETENTION_DAYS = 30;
|
||||
9
packages/shared/tsconfig.json
Normal file
9
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
21
tsconfig.base.json
Normal file
21
tsconfig.base.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"verbatimModuleSyntax": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user