Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cursor/rules/corates.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Do not worry about migrations either claient side or backend unless specifically

## Coding Standards

- Do not use emojis in code, comments, documentation, or commit messages.
- Do not use emojis in code, comments, documentation, plan files, or commit messages.
- For UI icons, use the `solid-icons` library or SVGs only. Do not use emojis.
- Follow standard JavaScript/SolidJS/Cloudflare best practices.
- Prefer modern ES6+ syntax and features.
Expand Down
51 changes: 51 additions & 0 deletions .cursor/rules/workers.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
alwaysApply: true
scope: packages/workers
---

# Workers Package Rules

## Drizzle Transactions

**ALWAYS use `db.batch()` for multiple related database operations** to ensure atomicity:

```javascript
// ✅ DO: Use batch for related operations
const batchOps = [
db.insert(projects).values({ id, name, createdBy }),
db.insert(projectMembers).values({ projectId: id, userId, role: 'owner' }),
];
await db.batch(batchOps);

// ❌ DON'T: Separate operations
await db.insert(projects).values({ id, name });
await db.insert(projectMembers).values({ projectId: id, userId });
```

Use batch when operations must be atomic (all succeed or all fail). Single independent operations don't need batch.

## Zod Validation

**ALWAYS validate request bodies** using `validateRequest` middleware:

```javascript
// ✅ DO: Use middleware
import { validateRequest, projectSchemas } from '../config/validation.js';

projectRoutes.post('/', validateRequest(projectSchemas.create), async c => {
const data = c.get('validatedBody'); // Already validated
});

// ❌ DON'T: Manual validation
projectRoutes.post('/', async c => {
const body = await c.req.json();
// Manual checks...
});
```

**Add new schemas to `config/validation.js`** and reuse `commonFields` when possible. Use `validateQueryParams` for query parameters.

## Examples

- See `src/routes/account-merge.js` for batch usage
- See `src/routes/projects.js` for validation patterns
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# IDE specific
.cursor/plans*

# dependencies
node_modules
.pnp
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
"./errors": {
"types": "./dist/errors/index.d.ts",
"import": "./dist/errors/index.js"
},
"./plans": {
"types": "./dist/plans/index.d.ts",
"import": "./dist/plans/index.js"
}
},
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion packages/shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* Main entry point for @corates/shared package
* Re-exports everything from errors module
* Re-exports everything from errors and plans modules
*/

export * from './errors/index.js';
export * from './plans/index.js';
18 changes: 18 additions & 0 deletions packages/shared/src/plans/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Public API exports for plans module
* This is the main entry point for consuming plan configuration
*/

// Types
export type {
PlanId,
EntitlementKey,
QuotaKey,
Entitlements,
Quotas,
Plan,
Plans,
} from './types.js';

// Plan configuration
export { PLANS, DEFAULT_PLAN, getPlan, isUnlimitedQuota } from './plans.js';
81 changes: 81 additions & 0 deletions packages/shared/src/plans/plans.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Plan configuration
* Maps plans to entitlements (boolean capabilities) and quotas (numeric limits)
* Plans are static configuration - not stored in database
*/

import type { Plans, PlanId } from './types.js';

/**
* Plan configurations for all subscription tiers
*/
export const PLANS: Plans = {
free: {
name: 'Free',
entitlements: {
'project.create': false,
'checklist.edit': true,
'export.pdf': false,
'ai.run': false,
},
quotas: {
'projects.max': 0,
'storage.project.maxMB': 10,
'ai.tokens.monthly': 0,
},
},
pro: {
name: 'Pro',
entitlements: {
'project.create': true,
'checklist.edit': true,
'export.pdf': true,
'ai.run': true,
},
quotas: {
'projects.max': 10,
'storage.project.maxMB': 1000,
'ai.tokens.monthly': 100000,
},
},
unlimited: {
name: 'Unlimited',
entitlements: {
'project.create': true,
'checklist.edit': true,
'export.pdf': true,
'ai.run': true,
},
quotas: {
'projects.max': -1,
'storage.project.maxMB': -1,
'ai.tokens.monthly': -1,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
};

/**
* Default plan ID for users without an active subscription
*/
export const DEFAULT_PLAN: PlanId = 'free';

/**
* Get plan configuration by plan ID
* @param planId - Plan ID (e.g., 'free', 'pro', 'unlimited')
* @returns Plan configuration, or default plan if planId is invalid
*/
export function getPlan(planId: PlanId | string): Plans[PlanId] {
if (planId in PLANS) {
return PLANS[planId as PlanId];
}
return PLANS[DEFAULT_PLAN];
}

/**
* Check if a quota value represents unlimited quota
* @param quota - Quota value to check
* @returns True if quota is unlimited (value is -1)
*/
export function isUnlimitedQuota(quota: number): boolean {
return quota === -1;
}
53 changes: 53 additions & 0 deletions packages/shared/src/plans/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Type definitions for plan configuration
* Defines types for plan IDs, entitlement keys, quota keys, and plan structure
*/

/**
* Plan ID - identifies a subscription tier
*/
export type PlanId = 'free' | 'pro' | 'unlimited';

/**
* Entitlement keys - boolean capabilities that can be enabled/disabled per plan
*/
export type EntitlementKey = 'project.create' | 'checklist.edit' | 'export.pdf' | 'ai.run';

/**
* Quota keys - numeric limits that can be set per plan
*/
export type QuotaKey = 'projects.max' | 'storage.project.maxMB' | 'ai.tokens.monthly';

/**
* Entitlements - mapping of entitlement keys to boolean values
*/
export interface Entitlements {
'project.create': boolean;
'checklist.edit': boolean;
'export.pdf': boolean;
'ai.run': boolean;
}

/**
* Quotas - mapping of quota keys to numeric values
* A value of -1 indicates unlimited quota (JSON-safe alternative to Infinity)
*/
export interface Quotas {
'projects.max': number;
'storage.project.maxMB': number;
'ai.tokens.monthly': number;
}

/**
* Plan configuration - defines entitlements and quotas for a subscription tier
*/
export interface Plan {
name: string;
entitlements: Entitlements;
quotas: Quotas;
}

/**
* Plans - record of all plan configurations indexed by plan ID
*/
export type Plans = Record<PlanId, Plan>;
1 change: 0 additions & 1 deletion packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
"@solidjs/router": "^0.15.4",
"better-auth": "1.4.5",
"d3": "^7.9.0",
"nanoid": "^5.1.6",
"pdfjs-dist": "^5.4.449",
"solid-icons": "^1.1.0",
"solid-js": "^1.9.10",
Expand Down
Loading