feat: lint codebase and run precommit hook
This commit is contained in:
@@ -0,0 +1 @@
|
||||
static
|
||||
@@ -56,9 +56,9 @@ GET /lessons (which course?)
|
||||
|
||||
```typescript
|
||||
// ✅ OK - Top-level resources
|
||||
GET /account
|
||||
PUT /account/user
|
||||
GET /account/subscription
|
||||
GET / account;
|
||||
PUT / account / user;
|
||||
GET / account / subscription;
|
||||
```
|
||||
|
||||
---
|
||||
@@ -67,13 +67,13 @@ GET /account/subscription
|
||||
|
||||
Use HTTP methods correctly for their intended purpose:
|
||||
|
||||
| Method | Purpose | Idempotent | Response Body |
|
||||
|--------|---------|------------|---------------|
|
||||
| `GET` | Retrieve resource(s) | ✅ Yes | Always |
|
||||
| `POST` | Create new resource | ❌ No | Created resource |
|
||||
| `PUT` | Replace entire resource | ✅ Yes | Updated resource |
|
||||
| `PATCH` | Partial update | ✅ Yes | Updated resource |
|
||||
| `DELETE` | Remove resource | ✅ Yes | Usually empty (204) |
|
||||
| Method | Purpose | Idempotent | Response Body |
|
||||
| -------- | ----------------------- | ---------- | ------------------- |
|
||||
| `GET` | Retrieve resource(s) | ✅ Yes | Always |
|
||||
| `POST` | Create new resource | ❌ No | Created resource |
|
||||
| `PUT` | Replace entire resource | ✅ Yes | Updated resource |
|
||||
| `PATCH` | Partial update | ✅ Yes | Updated resource |
|
||||
| `DELETE` | Remove resource | ✅ Yes | Usually empty (204) |
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Correct method usage
|
||||
@@ -112,11 +112,13 @@ POST /lessons?courseId=123 // Relationship not obvious
|
||||
```
|
||||
|
||||
**When to nest:**
|
||||
|
||||
- ✅ Child resource only exists within parent context
|
||||
- ✅ Child resource ID is unique within parent (e.g., `lessonId` within `courseId`)
|
||||
- ✅ Operations typically scoped to parent
|
||||
|
||||
**When NOT to nest:**
|
||||
|
||||
- ❌ Resource can exist independently
|
||||
- ❌ Resource ID is globally unique
|
||||
- ❌ Operations span multiple parents
|
||||
@@ -154,6 +156,7 @@ GET /get-courses // Redundant verb
|
||||
**Pattern:** `POST /resource/:id/action`
|
||||
|
||||
**When to use:**
|
||||
|
||||
- ✅ State transitions (publish, unpublish, activate, cancel)
|
||||
- ✅ Side effects (clone, duplicate, export)
|
||||
- ✅ Complex operations that don't fit CRUD
|
||||
@@ -181,14 +184,14 @@ GET /courses/search/javascript
|
||||
|
||||
**Common query parameter patterns:**
|
||||
|
||||
| Purpose | Parameter | Example |
|
||||
|---------|-----------|---------|
|
||||
| Filtering | `status`, `type`, `role` | `?status=active&type=course` |
|
||||
| Search | `q`, `search` | `?q=javascript` |
|
||||
| Pagination | `page`, `limit`, `offset` | `?page=1&limit=20` |
|
||||
| Sorting | `sort`, `order` | `?sort=created_at&order=desc` |
|
||||
| Date range | `startDate`, `endDate` | `?startDate=2024-01-01` |
|
||||
| Boolean flags | `include`, `expand` | `?include=lessons&expand=metadata` |
|
||||
| Purpose | Parameter | Example |
|
||||
| ------------- | ------------------------- | ---------------------------------- |
|
||||
| Filtering | `status`, `type`, `role` | `?status=active&type=course` |
|
||||
| Search | `q`, `search` | `?q=javascript` |
|
||||
| Pagination | `page`, `limit`, `offset` | `?page=1&limit=20` |
|
||||
| Sorting | `sort`, `order` | `?sort=created_at&order=desc` |
|
||||
| Date range | `startDate`, `endDate` | `?startDate=2024-01-01` |
|
||||
| Boolean flags | `include`, `expand` | `?include=lessons&expand=metadata` |
|
||||
|
||||
---
|
||||
|
||||
@@ -303,9 +306,9 @@ import { organizationRouter } from '@api/routes/organization';
|
||||
import { accountRouter } from '@api/routes/account';
|
||||
|
||||
export const app = new Hono()
|
||||
.route('/course', courseRouter) // /course/*
|
||||
.route('/course', courseRouter) // /course/*
|
||||
.route('/organization', organizationRouter) // /organization/*
|
||||
.route('/account', accountRouter); // /account/*
|
||||
.route('/account', accountRouter); // /account/*
|
||||
```
|
||||
|
||||
---
|
||||
@@ -321,37 +324,37 @@ export const courseRouter = new Hono()
|
||||
.get('/', authMiddleware, zValidator('query', ZListCourses), async (c) => {
|
||||
// GET /course?orgId=123&status=published&page=1&limit=20
|
||||
})
|
||||
|
||||
|
||||
// Get single course
|
||||
.get('/:courseId', authMiddleware, zValidator('param', ZGetCourse), async (c) => {
|
||||
// GET /course/:courseId
|
||||
})
|
||||
|
||||
|
||||
// Create course
|
||||
.post('/', authMiddleware, zValidator('json', ZCreateCourse), async (c) => {
|
||||
// POST /course
|
||||
})
|
||||
|
||||
|
||||
// Update course (full)
|
||||
.put('/:courseId', authMiddleware, zValidator('json', ZUpdateCourse), async (c) => {
|
||||
// PUT /course/:courseId
|
||||
})
|
||||
|
||||
|
||||
// Update course (partial)
|
||||
.patch('/:courseId', authMiddleware, zValidator('json', ZPatchCourse), async (c) => {
|
||||
// PATCH /course/:courseId
|
||||
})
|
||||
|
||||
|
||||
// Delete course
|
||||
.delete('/:courseId', authMiddleware, zValidator('param', ZDeleteCourse), async (c) => {
|
||||
// DELETE /course/:courseId
|
||||
})
|
||||
|
||||
|
||||
// Actions
|
||||
.post('/:courseId/clone', authMiddleware, zValidator('json', ZCloneCourse), async (c) => {
|
||||
// POST /course/:courseId/clone
|
||||
})
|
||||
|
||||
|
||||
.post('/:courseId/publish', authMiddleware, zValidator('param', ZPublishCourse), async (c) => {
|
||||
// POST /course/:courseId/publish
|
||||
});
|
||||
@@ -366,22 +369,22 @@ export const lessonRouter = new Hono()
|
||||
.get('/', authMiddleware, zValidator('query', ZListLessons), async (c) => {
|
||||
// GET /course/:courseId/lesson?page=1&limit=20
|
||||
})
|
||||
|
||||
|
||||
// Get single lesson
|
||||
.get('/:lessonId', authMiddleware, zValidator('param', ZGetLesson), async (c) => {
|
||||
// GET /course/:courseId/lesson/:lessonId
|
||||
})
|
||||
|
||||
|
||||
// Create lesson
|
||||
.post('/', authMiddleware, zValidator('json', ZCreateLesson), async (c) => {
|
||||
// POST /course/:courseId/lesson
|
||||
})
|
||||
|
||||
|
||||
// Update lesson
|
||||
.put('/:lessonId', authMiddleware, zValidator('json', ZUpdateLesson), async (c) => {
|
||||
// PUT /course/:courseId/lesson/:lessonId
|
||||
})
|
||||
|
||||
|
||||
// Delete lesson
|
||||
.delete('/:lessonId', authMiddleware, zValidator('param', ZDeleteLesson), async (c) => {
|
||||
// DELETE /course/:courseId/lesson/:lessonId
|
||||
@@ -391,7 +394,7 @@ export const lessonRouter = new Hono()
|
||||
// apps/api/src/routes/course/course.ts
|
||||
export const courseRouter = new Hono()
|
||||
// ... course routes ...
|
||||
.route('/:courseId/lesson', lessonRouter); // Nested under course
|
||||
.route('/:courseId/lesson', lessonRouter); // Nested under course
|
||||
```
|
||||
|
||||
### Example 3: Organization with Team
|
||||
@@ -403,31 +406,31 @@ export const organizationRouter = new Hono()
|
||||
.get('/', authMiddleware, zValidator('query', ZListOrgs), async (c) => {
|
||||
// GET /organization?siteName=acme
|
||||
})
|
||||
|
||||
|
||||
// Get organization
|
||||
.get('/:orgId', authMiddleware, zValidator('param', ZGetOrg), async (c) => {
|
||||
// GET /organization/:orgId
|
||||
})
|
||||
|
||||
|
||||
// Create organization
|
||||
.post('/', authMiddleware, zValidator('json', ZCreateOrg), async (c) => {
|
||||
// POST /organization
|
||||
})
|
||||
|
||||
|
||||
// Update organization
|
||||
.put('/:orgId', authMiddleware, zValidator('json', ZUpdateOrg), async (c) => {
|
||||
// PUT /organization/:orgId
|
||||
})
|
||||
|
||||
|
||||
// Team management (nested)
|
||||
.get('/:orgId/team', authMiddleware, zValidator('param', ZGetTeam), async (c) => {
|
||||
// GET /organization/:orgId/team
|
||||
})
|
||||
|
||||
|
||||
.post('/:orgId/team/invite', authMiddleware, zValidator('json', ZInviteTeam), async (c) => {
|
||||
// POST /organization/:orgId/team/invite
|
||||
})
|
||||
|
||||
|
||||
.delete('/:orgId/team/:memberId', authMiddleware, zValidator('param', ZRemoveMember), async (c) => {
|
||||
// DELETE /organization/:orgId/team/:memberId
|
||||
});
|
||||
@@ -457,16 +460,16 @@ DELETE /courses/:id
|
||||
|
||||
```typescript
|
||||
// ❌ BAD
|
||||
GET /course
|
||||
POST /courses
|
||||
GET /organization
|
||||
POST /organizations
|
||||
GET / course;
|
||||
POST / courses;
|
||||
GET / organization;
|
||||
POST / organizations;
|
||||
|
||||
// ✅ GOOD
|
||||
GET /courses
|
||||
POST /courses
|
||||
GET /organizations
|
||||
POST /organizations
|
||||
GET / courses;
|
||||
POST / courses;
|
||||
GET / organizations;
|
||||
POST / organizations;
|
||||
```
|
||||
|
||||
### ❌ Don't: Use query parameters for required identifiers
|
||||
@@ -540,4 +543,3 @@ When creating a new route, ensure:
|
||||
8. **Organized by domain** in code structure
|
||||
|
||||
Following these conventions ensures your API is **predictable**, **maintainable**, and **developer-friendly**.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"test": "vitest NODE_ENV=test",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"format": "prettier --write .",
|
||||
"upload:openapi": "tsx scripts/upload-openapi-spec.ts"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
@@ -11,6 +11,7 @@ pnpm run upload:openapi
|
||||
```
|
||||
|
||||
### Direct script usage
|
||||
|
||||
```bash
|
||||
tsx scripts/upload-openapi-spec.ts
|
||||
```
|
||||
@@ -49,6 +50,7 @@ This script is integrated with a GitHub Action that automatically generates and
|
||||
### Automatic Workflow
|
||||
|
||||
The GitHub Action runs on:
|
||||
|
||||
- **Push to main** - Automatically uploads to R2
|
||||
- **Pull Requests** - Generates spec for review (no upload)
|
||||
- **Manual dispatch** - Allows manual triggering with custom parameters
|
||||
@@ -57,7 +59,7 @@ The GitHub Action runs on:
|
||||
|
||||
1. **Required secrets:**
|
||||
- `CLOUDFLARE_ACCESS_KEY`
|
||||
- `CLOUDFLARE_SECRET_ACCESS_KEY`
|
||||
- `CLOUDFLARE_SECRET_ACCESS_KEY`
|
||||
- `CLOUDFLARE_ACCOUNT_ID`
|
||||
- `PUBLIC_SUPABASE_URL`
|
||||
- `PUBLIC_SUPABASE_ANON_KEY`
|
||||
|
||||
@@ -24,11 +24,7 @@ class OpenAPISpecGenerator {
|
||||
}
|
||||
|
||||
private initializeS3Client() {
|
||||
if (
|
||||
env.CLOUDFLARE_ACCESS_KEY &&
|
||||
env.CLOUDFLARE_SECRET_ACCESS_KEY &&
|
||||
env.CLOUDFLARE_ACCOUNT_ID
|
||||
) {
|
||||
if (env.CLOUDFLARE_ACCESS_KEY && env.CLOUDFLARE_SECRET_ACCESS_KEY && env.CLOUDFLARE_ACCOUNT_ID) {
|
||||
this.s3Client = new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
|
||||
@@ -36,7 +36,6 @@ export const createRateLimiter = (options: RateLimiterOptions = {}): MiddlewareH
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
// Generate rate limit key
|
||||
const key = opts.keyGenerator(c);
|
||||
|
||||
|
||||
@@ -13,19 +13,13 @@ export class RedisRateLimiter {
|
||||
private windowMs: number;
|
||||
private maxRequests: number;
|
||||
|
||||
constructor(
|
||||
redis: Redis,
|
||||
windowMs: number = DEFAULT_WINDOW_MS,
|
||||
maxRequests: number = DEFAULT_MAX_REQUESTS
|
||||
) {
|
||||
constructor(redis: Redis, windowMs: number = DEFAULT_WINDOW_MS, maxRequests: number = DEFAULT_MAX_REQUESTS) {
|
||||
this.redis = redis;
|
||||
this.windowMs = windowMs;
|
||||
this.maxRequests = maxRequests;
|
||||
}
|
||||
|
||||
async isAllowed(
|
||||
key: string
|
||||
): Promise<{ allowed: boolean; remaining: number; resetTime: number }> {
|
||||
async isAllowed(key: string): Promise<{ allowed: boolean; remaining: number; resetTime: number }> {
|
||||
const now = Date.now();
|
||||
const windowStart = now - this.windowMs;
|
||||
const keyWithPrefix = `${RATE_LIMIT_KEY_PREFIX}${key}`;
|
||||
|
||||
@@ -154,20 +154,23 @@
|
||||
<section>
|
||||
<h1 class="text-xl font-bold my-1">Extra Resources</h1>
|
||||
<div class="flex mt-2 mb-1">
|
||||
<p class="m-0 mr-2">Slide Link:</p> <a href=https://youtu.be/dyLaKhePzj8 style="color: blue"
|
||||
<p class="m-0 mr-2">Slide Link:</p> <a href="https://youtu.be/dyLaKhePzj8" style="color: blue"
|
||||
class="underline">Open Here</a>
|
||||
</div>
|
||||
|
||||
<p class="m-0">Video Link:</p>
|
||||
<div class="m-0">
|
||||
<ol class="m-0">
|
||||
|
||||
<li key={index + 1}><a href=https://youtu.be/0fYi8SGA20k style="color: blue"
|
||||
class="underline">https://youtu.be/0fYi8SGA20k</a></li>
|
||||
|
||||
<li key={index + 1}><a href=https://youtu.be/0fYi8SGA20k style="color: blue"
|
||||
class="underline">https://youtu.be/0fYi8SGA20k</a></li>
|
||||
|
||||
<li>
|
||||
<a href="https://youtu.be/0fYi8SGA20k" style="color: blue" class="underline">
|
||||
https://youtu.be/0fYi8SGA20k
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://youtu.be/0fYi8SGA20k" style="color: blue" class="underline">
|
||||
https://youtu.be/0fYi8SGA20k
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -158,20 +158,25 @@
|
||||
<section>
|
||||
<h1 class="text-xl font-bold my-1">Extra Resources</h1>
|
||||
<div class="flex mt-2 mb-1">
|
||||
<p class="m-0 mr-2">Slide Link:</p> <a href=https://youtu.be/dyLaKhePzj8 style="color: blue"
|
||||
class="underline">Open Here</a>
|
||||
<p class="m-0 mr-2">Slide Link:</p>
|
||||
<a href="https://youtu.be/dyLaKhePzj8" style="color: blue" class="underline">
|
||||
Open Here
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="m-0">Video Link:</p>
|
||||
<div class="m-0">
|
||||
<ol class="m-0">
|
||||
|
||||
<li key={index + 1}><a href=https://youtu.be/0fYi8SGA20k style="color: blue"
|
||||
class="underline">https://youtu.be/0fYi8SGA20k</a></li>
|
||||
|
||||
<li key={index + 1}><a href=https://youtu.be/0fYi8SGA20k style="color: blue"
|
||||
class="underline">https://youtu.be/0fYi8SGA20k</a></li>
|
||||
|
||||
<li>
|
||||
<a href="https://youtu.be/0fYi8SGA20k" style="color: blue" class="underline">
|
||||
https://youtu.be/0fYi8SGA20k
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://youtu.be/0fYi8SGA20k" style="color: blue" class="underline">
|
||||
https://youtu.be/0fYi8SGA20k
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user