@levironexe/architect 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/CONTRIBUTING.md +55 -0
- package/README.md +341 -0
- package/dist/analyzers/ast-parser.d.ts +3 -0
- package/dist/analyzers/ast-parser.js +305 -0
- package/dist/analyzers/ast-parser.js.map +1 -0
- package/dist/analyzers/dependency-graph.d.ts +2 -0
- package/dist/analyzers/dependency-graph.js +67 -0
- package/dist/analyzers/dependency-graph.js.map +1 -0
- package/dist/analyzers/duplication.d.ts +2 -0
- package/dist/analyzers/duplication.js +56 -0
- package/dist/analyzers/duplication.js.map +1 -0
- package/dist/analyzers/file-walker.d.ts +3 -0
- package/dist/analyzers/file-walker.js +80 -0
- package/dist/analyzers/file-walker.js.map +1 -0
- package/dist/cli/context-runner.d.ts +1 -0
- package/dist/cli/context-runner.js +16 -0
- package/dist/cli/context-runner.js.map +1 -0
- package/dist/cli/index.d.ts +24 -0
- package/dist/cli/index.js +217 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-runner.d.ts +25 -0
- package/dist/cli/init-runner.js +152 -0
- package/dist/cli/init-runner.js.map +1 -0
- package/dist/cli/scan-runner.d.ts +8 -0
- package/dist/cli/scan-runner.js +133 -0
- package/dist/cli/scan-runner.js.map +1 -0
- package/dist/formatters/plan-json.d.ts +2 -0
- package/dist/formatters/plan-json.js +4 -0
- package/dist/formatters/plan-json.js.map +1 -0
- package/dist/formatters/plan-markdown.d.ts +2 -0
- package/dist/formatters/plan-markdown.js +42 -0
- package/dist/formatters/plan-markdown.js.map +1 -0
- package/dist/formatters/plan-prompt.d.ts +4 -0
- package/dist/formatters/plan-prompt.js +5 -0
- package/dist/formatters/plan-prompt.js.map +1 -0
- package/dist/formatters/plan-terminal.d.ts +5 -0
- package/dist/formatters/plan-terminal.js +62 -0
- package/dist/formatters/plan-terminal.js.map +1 -0
- package/dist/generators/blueprint-renderer.d.ts +3 -0
- package/dist/generators/blueprint-renderer.js +27 -0
- package/dist/generators/blueprint-renderer.js.map +1 -0
- package/dist/generators/claudeWriter.d.ts +3 -0
- package/dist/generators/claudeWriter.js +9 -0
- package/dist/generators/claudeWriter.js.map +1 -0
- package/dist/generators/copilotWriter.d.ts +3 -0
- package/dist/generators/copilotWriter.js +11 -0
- package/dist/generators/copilotWriter.js.map +1 -0
- package/dist/generators/cursorWriter.d.ts +3 -0
- package/dist/generators/cursorWriter.js +14 -0
- package/dist/generators/cursorWriter.js.map +1 -0
- package/dist/generators/genericWriter.d.ts +3 -0
- package/dist/generators/genericWriter.js +9 -0
- package/dist/generators/genericWriter.js.map +1 -0
- package/dist/generators/template-context.d.ts +18 -0
- package/dist/generators/template-context.js +126 -0
- package/dist/generators/template-context.js.map +1 -0
- package/dist/generators/templateRenderer.d.ts +2 -0
- package/dist/generators/templateRenderer.js +19 -0
- package/dist/generators/templateRenderer.js.map +1 -0
- package/dist/generators/windsurfWriter.d.ts +3 -0
- package/dist/generators/windsurfWriter.js +14 -0
- package/dist/generators/windsurfWriter.js.map +1 -0
- package/dist/generators/writer-types.d.ts +11 -0
- package/dist/generators/writer-types.js +40 -0
- package/dist/generators/writer-types.js.map +1 -0
- package/dist/llm/claude-provider.d.ts +8 -0
- package/dist/llm/claude-provider.js +22 -0
- package/dist/llm/claude-provider.js.map +1 -0
- package/dist/llm/concern-classifier.d.ts +15 -0
- package/dist/llm/concern-classifier.js +61 -0
- package/dist/llm/concern-classifier.js.map +1 -0
- package/dist/llm/config.d.ts +11 -0
- package/dist/llm/config.js +120 -0
- package/dist/llm/config.js.map +1 -0
- package/dist/llm/ollama-provider.d.ts +8 -0
- package/dist/llm/ollama-provider.js +27 -0
- package/dist/llm/ollama-provider.js.map +1 -0
- package/dist/llm/openai-provider.d.ts +8 -0
- package/dist/llm/openai-provider.js +19 -0
- package/dist/llm/openai-provider.js.map +1 -0
- package/dist/llm/prompt-builder.d.ts +12 -0
- package/dist/llm/prompt-builder.js +132 -0
- package/dist/llm/prompt-builder.js.map +1 -0
- package/dist/llm/provider.d.ts +17 -0
- package/dist/llm/provider.js +2 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/llm/response-parser.d.ts +6 -0
- package/dist/llm/response-parser.js +128 -0
- package/dist/llm/response-parser.js.map +1 -0
- package/dist/planner/plan-generator.d.ts +7 -0
- package/dist/planner/plan-generator.js +275 -0
- package/dist/planner/plan-generator.js.map +1 -0
- package/dist/planner/plan-prompt-builder.d.ts +9 -0
- package/dist/planner/plan-prompt-builder.js +92 -0
- package/dist/planner/plan-prompt-builder.js.map +1 -0
- package/dist/planner/plan-response-parser.d.ts +7 -0
- package/dist/planner/plan-response-parser.js +21 -0
- package/dist/planner/plan-response-parser.js.map +1 -0
- package/dist/planner/plan-validator.d.ts +3 -0
- package/dist/planner/plan-validator.js +49 -0
- package/dist/planner/plan-validator.js.map +1 -0
- package/dist/reporters/scan-json.d.ts +13 -0
- package/dist/reporters/scan-json.js +26 -0
- package/dist/reporters/scan-json.js.map +1 -0
- package/dist/reporters/terminal.d.ts +6 -0
- package/dist/reporters/terminal.js +224 -0
- package/dist/reporters/terminal.js.map +1 -0
- package/dist/scoring/consistency-score.d.ts +3 -0
- package/dist/scoring/consistency-score.js +23 -0
- package/dist/scoring/consistency-score.js.map +1 -0
- package/dist/scoring/duplication-score.d.ts +3 -0
- package/dist/scoring/duplication-score.js +16 -0
- package/dist/scoring/duplication-score.js.map +1 -0
- package/dist/scoring/health-score.d.ts +4 -0
- package/dist/scoring/health-score.js +20 -0
- package/dist/scoring/health-score.js.map +1 -0
- package/dist/scoring/issue-builder.d.ts +4 -0
- package/dist/scoring/issue-builder.js +62 -0
- package/dist/scoring/issue-builder.js.map +1 -0
- package/dist/scoring/modularity-score.d.ts +3 -0
- package/dist/scoring/modularity-score.js +56 -0
- package/dist/scoring/modularity-score.js.map +1 -0
- package/dist/scoring/pattern-analysis.d.ts +3 -0
- package/dist/scoring/pattern-analysis.js +74 -0
- package/dist/scoring/pattern-analysis.js.map +1 -0
- package/dist/scoring/separation-score.d.ts +3 -0
- package/dist/scoring/separation-score.js +35 -0
- package/dist/scoring/separation-score.js.map +1 -0
- package/dist/skills/detector.d.ts +4 -0
- package/dist/skills/detector.js +104 -0
- package/dist/skills/detector.js.map +1 -0
- package/dist/skills/lister.d.ts +9 -0
- package/dist/skills/lister.js +35 -0
- package/dist/skills/lister.js.map +1 -0
- package/dist/skills/loader.d.ts +6 -0
- package/dist/skills/loader.js +76 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/structure-check.d.ts +2 -0
- package/dist/skills/structure-check.js +37 -0
- package/dist/skills/structure-check.js.map +1 -0
- package/dist/skills/validator.d.ts +6 -0
- package/dist/skills/validator.js +229 -0
- package/dist/skills/validator.js.map +1 -0
- package/dist/types/analysis.d.ts +130 -0
- package/dist/types/analysis.js +41 -0
- package/dist/types/analysis.js.map +1 -0
- package/dist/types/concern.d.ts +48 -0
- package/dist/types/concern.js +16 -0
- package/dist/types/concern.js.map +1 -0
- package/dist/types/generation.d.ts +32 -0
- package/dist/types/generation.js +2 -0
- package/dist/types/generation.js.map +1 -0
- package/dist/types/issue.d.ts +12 -0
- package/dist/types/issue.js +2 -0
- package/dist/types/issue.js.map +1 -0
- package/dist/types/pattern.d.ts +15 -0
- package/dist/types/pattern.js +2 -0
- package/dist/types/pattern.js.map +1 -0
- package/dist/types/plan.d.ts +56 -0
- package/dist/types/plan.js +2 -0
- package/dist/types/plan.js.map +1 -0
- package/dist/types/scan-output.d.ts +84 -0
- package/dist/types/scan-output.js +2 -0
- package/dist/types/scan-output.js.map +1 -0
- package/dist/types/scoring.d.ts +15 -0
- package/dist/types/scoring.js +2 -0
- package/dist/types/scoring.js.map +1 -0
- package/dist/types/skill.d.ts +97 -0
- package/dist/types/skill.js +2 -0
- package/dist/types/skill.js.map +1 -0
- package/dist/utils/agent-detector.d.ts +2 -0
- package/dist/utils/agent-detector.js +22 -0
- package/dist/utils/agent-detector.js.map +1 -0
- package/dist/utils/interactive.d.ts +6 -0
- package/dist/utils/interactive.js +15 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/path.d.ts +5 -0
- package/dist/utils/path.js +31 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/progress.d.ts +17 -0
- package/dist/utils/progress.js +48 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/utils/thresholds.d.ts +6 -0
- package/dist/utils/thresholds.js +48 -0
- package/dist/utils/thresholds.js.map +1 -0
- package/package.json +63 -0
- package/skills/meta/general-js.skill.yaml +131 -0
- package/skills/patterns/clerk-auth.skill.yaml +349 -0
- package/skills/patterns/docker-deploy.skill.yaml +214 -0
- package/skills/patterns/drizzle.skill.yaml +277 -0
- package/skills/patterns/mongoose.skill.yaml +290 -0
- package/skills/patterns/nextauth.skill.yaml +308 -0
- package/skills/patterns/playwright-e2e.skill.yaml +265 -0
- package/skills/patterns/prisma.skill.yaml +255 -0
- package/skills/patterns/s3-storage.skill.yaml +235 -0
- package/skills/patterns/selenium-e2e.skill.yaml +276 -0
- package/skills/patterns/supabase-auth.skill.yaml +298 -0
- package/skills/patterns/supabase.skill.yaml +304 -0
- package/skills/patterns/vercel-deploy.skill.yaml +219 -0
- package/skills/patterns/vitest-testing.skill.yaml +262 -0
- package/skills/stacks/express-api.skill.yaml +155 -0
- package/skills/stacks/fastify-api.skill.yaml +119 -0
- package/skills/stacks/hono-api.skill.yaml +130 -0
- package/skills/stacks/nestjs.skill.yaml +135 -0
- package/skills/stacks/nextjs-app-router.skill.yaml +176 -0
- package/skills/stacks/react-spa.skill.yaml +153 -0
- package/skills/stacks/vue-nuxt.skill.yaml +115 -0
- package/templates/architect-plan.md +139 -0
- package/templates/architect-refactor.md +119 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
schema_version: "2.0.0"
|
|
2
|
+
id: drizzle
|
|
3
|
+
name: "Drizzle ORM"
|
|
4
|
+
version: "2.0.0"
|
|
5
|
+
description: "SQL-first Drizzle ORM with TypeScript schema as source of truth, drizzle-kit migrations, singleton database connection, inferred types, and explicit query building."
|
|
6
|
+
category: pattern
|
|
7
|
+
language: javascript
|
|
8
|
+
frameworks:
|
|
9
|
+
- drizzle-orm
|
|
10
|
+
dependencies:
|
|
11
|
+
none:
|
|
12
|
+
- prisma
|
|
13
|
+
- "@prisma/client"
|
|
14
|
+
- mongoose
|
|
15
|
+
detection:
|
|
16
|
+
dependencies:
|
|
17
|
+
any:
|
|
18
|
+
- drizzle-orm
|
|
19
|
+
source_indicators:
|
|
20
|
+
- "drizzle("
|
|
21
|
+
- "pgTable("
|
|
22
|
+
- "mysqlTable("
|
|
23
|
+
- "sqliteTable("
|
|
24
|
+
- "from 'drizzle-orm'"
|
|
25
|
+
structure:
|
|
26
|
+
required_dirs:
|
|
27
|
+
- path: src/db
|
|
28
|
+
purpose: "Drizzle schema definitions in schema.ts — the single source of truth for all table structures and TypeScript types. Every table, column, index, and relation is defined here as TypeScript. The generated types (via `typeof users.$inferSelect`) come from these definitions — never from separate type files."
|
|
29
|
+
- path: drizzle
|
|
30
|
+
purpose: "Generated migration SQL files created by `npx drizzle-kit generate`. These files represent the canonical migration history. Never edit them manually — drizzle-kit detects checksum changes and may regenerate or fail on the next run."
|
|
31
|
+
recommended_dirs:
|
|
32
|
+
- path: src/lib
|
|
33
|
+
purpose: "Drizzle database connection singleton in db.ts — the only place that creates the database connection and passes it to drizzle(). Imported by all repository files. Prevents multiple connections in serverless environments with the same global caching pattern as Prisma."
|
|
34
|
+
- path: src/db/queries
|
|
35
|
+
purpose: "Reusable query functions organized by resource — e.g. queries/users.ts, queries/posts.ts. Functions use Drizzle's query builder API and return typed results. Services and API routes call these functions rather than building queries inline."
|
|
36
|
+
separation:
|
|
37
|
+
rules:
|
|
38
|
+
- concern: schema_definition
|
|
39
|
+
belongs_in: src/db
|
|
40
|
+
rule_text: "Define all table schemas in src/db/schema.ts using Drizzle's TypeScript API. Export the table objects so that drizzle-kit can discover them for migration generation. Use `typeof table.$inferSelect` and `typeof table.$inferInsert` for type inference — never manually write type interfaces for database rows."
|
|
41
|
+
example: |
|
|
42
|
+
// src/db/schema.ts — single source of truth
|
|
43
|
+
import { pgTable, text, timestamp, uuid, boolean } from 'drizzle-orm/pg-core';
|
|
44
|
+
|
|
45
|
+
export const users = pgTable('users', {
|
|
46
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
47
|
+
email: text('email').notNull().unique(),
|
|
48
|
+
name: text('name'),
|
|
49
|
+
emailVerified: boolean('email_verified').notNull().default(false),
|
|
50
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
51
|
+
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const posts = pgTable('posts', {
|
|
55
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
56
|
+
title: text('title').notNull(),
|
|
57
|
+
content: text('content'),
|
|
58
|
+
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
59
|
+
publishedAt: timestamp('published_at'),
|
|
60
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Inferred types — never write these manually
|
|
64
|
+
export type User = typeof users.$inferSelect;
|
|
65
|
+
export type NewUser = typeof users.$inferInsert;
|
|
66
|
+
export type Post = typeof posts.$inferSelect;
|
|
67
|
+
export type NewPost = typeof posts.$inferInsert;
|
|
68
|
+
indicators:
|
|
69
|
+
- "pgTable("
|
|
70
|
+
- "mysqlTable("
|
|
71
|
+
- "sqliteTable("
|
|
72
|
+
- "$inferSelect"
|
|
73
|
+
- "$inferInsert"
|
|
74
|
+
- concern: database_connection
|
|
75
|
+
belongs_in: src/lib
|
|
76
|
+
rule_text: "Create the Drizzle database connection in src/lib/db.ts using a module-level or global singleton. Pass the underlying driver instance (e.g., postgres from 'postgres' or Pool from 'pg') to drizzle(). Never create a new connection per query or per request."
|
|
77
|
+
example: |
|
|
78
|
+
// src/lib/db.ts — singleton pattern for serverless compatibility
|
|
79
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
80
|
+
import postgres from 'postgres';
|
|
81
|
+
import * as schema from '@/db/schema';
|
|
82
|
+
|
|
83
|
+
// Singleton: reuse connection across hot reloads in development
|
|
84
|
+
const globalForDb = globalThis as unknown as {
|
|
85
|
+
connection: ReturnType<typeof postgres> | undefined;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const connection =
|
|
89
|
+
globalForDb.connection ??
|
|
90
|
+
postgres(process.env.DATABASE_URL!, {
|
|
91
|
+
max: process.env.NODE_ENV === 'production' ? 10 : 1,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (process.env.NODE_ENV !== 'production') globalForDb.connection = connection;
|
|
95
|
+
|
|
96
|
+
export const db = drizzle(connection, { schema });
|
|
97
|
+
indicators:
|
|
98
|
+
- "drizzle("
|
|
99
|
+
- "DATABASE_URL"
|
|
100
|
+
- "from 'drizzle-orm"
|
|
101
|
+
- concern: migrations
|
|
102
|
+
belongs_in: drizzle
|
|
103
|
+
rule_text: "Use drizzle-kit to generate and apply migrations. Configure drizzle.config.ts with the schema path and migration output directory. Run `npx drizzle-kit generate` to create migration SQL from schema changes, and `npx drizzle-kit migrate` (or `push` for prototyping) to apply them."
|
|
104
|
+
example: |
|
|
105
|
+
// drizzle.config.ts — tells drizzle-kit where to find schema and write migrations
|
|
106
|
+
import { defineConfig } from 'drizzle-kit';
|
|
107
|
+
export default defineConfig({
|
|
108
|
+
schema: './src/db/schema.ts',
|
|
109
|
+
out: './drizzle',
|
|
110
|
+
dialect: 'postgresql',
|
|
111
|
+
dbCredentials: { url: process.env.DATABASE_URL! },
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
# Development workflow:
|
|
115
|
+
# 1. Edit src/db/schema.ts
|
|
116
|
+
# 2. Generate migration:
|
|
117
|
+
npx drizzle-kit generate
|
|
118
|
+
# 3. Apply migration:
|
|
119
|
+
npx drizzle-kit migrate
|
|
120
|
+
# 4. The db object's types update automatically (schema is the source)
|
|
121
|
+
indicators:
|
|
122
|
+
- "drizzle-kit generate"
|
|
123
|
+
- "drizzle-kit migrate"
|
|
124
|
+
- "drizzle.config.ts"
|
|
125
|
+
- "defineConfig("
|
|
126
|
+
- concern: query_building
|
|
127
|
+
belongs_in: src/db/queries
|
|
128
|
+
rule_text: "Use Drizzle's query builder API for all queries. For complex SQL, use the `sql` template tag with parameterized values — never string concatenation. Queries live in src/db/queries/ functions; services call these functions rather than building queries inline."
|
|
129
|
+
example: |
|
|
130
|
+
// src/db/queries/users.ts
|
|
131
|
+
import { db } from '@/lib/db';
|
|
132
|
+
import { users, posts } from '@/db/schema';
|
|
133
|
+
import { eq, and, desc, ilike, sql } from 'drizzle-orm';
|
|
134
|
+
|
|
135
|
+
export async function findUserByEmail(email: string) {
|
|
136
|
+
return db.query.users.findFirst({
|
|
137
|
+
where: eq(users.email, email),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function listUsersWithPostCount(limit = 20) {
|
|
142
|
+
return db
|
|
143
|
+
.select({
|
|
144
|
+
id: users.id,
|
|
145
|
+
email: users.email,
|
|
146
|
+
name: users.name,
|
|
147
|
+
postCount: sql<number>`count(${posts.id})`.mapWith(Number),
|
|
148
|
+
})
|
|
149
|
+
.from(users)
|
|
150
|
+
.leftJoin(posts, eq(posts.userId, users.id))
|
|
151
|
+
.groupBy(users.id)
|
|
152
|
+
.orderBy(desc(users.createdAt))
|
|
153
|
+
.limit(limit);
|
|
154
|
+
}
|
|
155
|
+
anti_indicators:
|
|
156
|
+
- "db.execute(`SELECT"
|
|
157
|
+
- "db.execute(\"SELECT"
|
|
158
|
+
- "+ email +"
|
|
159
|
+
- concern: transactions
|
|
160
|
+
belongs_in: src/db/queries
|
|
161
|
+
rule_text: "Use db.transaction() for operations that must succeed or fail atomically. The transaction callback receives a `tx` context — use `tx` instead of `db` for all queries inside the transaction."
|
|
162
|
+
example: |
|
|
163
|
+
// src/db/queries/orders.ts
|
|
164
|
+
import { db } from '@/lib/db';
|
|
165
|
+
import { orders, inventory } from '@/db/schema';
|
|
166
|
+
import { eq, sql } from 'drizzle-orm';
|
|
167
|
+
|
|
168
|
+
export async function createOrderWithInventoryDecrement(
|
|
169
|
+
userId: string,
|
|
170
|
+
productId: string,
|
|
171
|
+
quantity: number,
|
|
172
|
+
price: number
|
|
173
|
+
) {
|
|
174
|
+
return db.transaction(async (tx) => {
|
|
175
|
+
// Atomically decrement inventory
|
|
176
|
+
const [product] = await tx
|
|
177
|
+
.update(inventory)
|
|
178
|
+
.set({ stock: sql`${inventory.stock} - ${quantity}` })
|
|
179
|
+
.where(and(eq(inventory.productId, productId), sql`${inventory.stock} >= ${quantity}`))
|
|
180
|
+
.returning();
|
|
181
|
+
|
|
182
|
+
if (!product) throw new Error('Insufficient stock');
|
|
183
|
+
|
|
184
|
+
return tx.insert(orders).values({ userId, productId, quantity, total: price * quantity }).returning();
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
indicators:
|
|
188
|
+
- "db.transaction("
|
|
189
|
+
- "tx.insert("
|
|
190
|
+
- "tx.update("
|
|
191
|
+
patterns:
|
|
192
|
+
data_flow:
|
|
193
|
+
direction: "API Route/Server Action → Service → Query Function (src/db/queries/) → Drizzle → Database"
|
|
194
|
+
rules:
|
|
195
|
+
- "src/db/schema.ts is the source of truth — TypeScript types are inferred from it, never written manually."
|
|
196
|
+
- "db in src/lib/db.ts is the only Drizzle instance — imported by all query files."
|
|
197
|
+
- "Query functions in src/db/queries/ own all SQL — services call these functions, never db.select() directly."
|
|
198
|
+
- "Migrations are generated by drizzle-kit from schema changes — never write migration SQL manually."
|
|
199
|
+
- "Use $inferSelect and $inferInsert for parameter and return types — keeps types in sync with schema automatically."
|
|
200
|
+
error_handling:
|
|
201
|
+
recommended: "Drizzle does not wrap errors — database errors bubble as driver-level errors. Check error.code: '23505' = PostgreSQL unique violation, '23503' = foreign key violation. Wrap in try/catch in repositories."
|
|
202
|
+
naming:
|
|
203
|
+
schema: "src/db/schema.ts — all pgTable/mysqlTable definitions with $inferSelect/$inferInsert exports"
|
|
204
|
+
connection: "src/lib/db.ts — drizzle(connection, { schema }) singleton"
|
|
205
|
+
queries: "src/db/queries/[resource].ts — e.g. users.ts, posts.ts"
|
|
206
|
+
config: "drizzle.config.ts — drizzle-kit configuration at project root"
|
|
207
|
+
anti_patterns:
|
|
208
|
+
- id: raw_sql_strings
|
|
209
|
+
severity: warning
|
|
210
|
+
description: "Building SQL queries with string concatenation or unparameterized template literals. Even with Drizzle available, some developers fall back to raw strings when queries get complex. This loses type safety and creates SQL injection risk."
|
|
211
|
+
bad_example: |
|
|
212
|
+
// ❌ String concatenation — SQL injection risk, no type safety
|
|
213
|
+
const email = req.body.email;
|
|
214
|
+
await db.execute(`SELECT * FROM users WHERE email = '${email}'`);
|
|
215
|
+
// If email = "' OR '1'='1" → returns all users
|
|
216
|
+
good_example: |
|
|
217
|
+
// ✓ Drizzle query builder — parameterized and type-safe
|
|
218
|
+
import { eq } from 'drizzle-orm';
|
|
219
|
+
const user = await db.select().from(users).where(eq(users.email, email));
|
|
220
|
+
|
|
221
|
+
// ✓ For complex SQL: use sql tag with parameters (not string concat)
|
|
222
|
+
import { sql } from 'drizzle-orm';
|
|
223
|
+
const result = await db.execute(sql`SELECT * FROM users WHERE email = ${email}`);
|
|
224
|
+
- id: multiple_db_connections
|
|
225
|
+
severity: critical
|
|
226
|
+
description: "Creating a new Drizzle instance (and underlying driver connection/pool) in multiple files instead of importing from src/lib/db.ts. In serverless environments, each function invocation may create a new connection pool, exhausting database connections under load."
|
|
227
|
+
bad_example: |
|
|
228
|
+
// ❌ New drizzle instance in every repository file
|
|
229
|
+
// src/db/queries/users.ts
|
|
230
|
+
import postgres from 'postgres';
|
|
231
|
+
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
232
|
+
const db = drizzle(postgres(process.env.DATABASE_URL!)); // new connection pool
|
|
233
|
+
|
|
234
|
+
// src/db/queries/posts.ts
|
|
235
|
+
const db = drizzle(postgres(process.env.DATABASE_URL!)); // another pool
|
|
236
|
+
good_example: |
|
|
237
|
+
// ✓ Import the singleton from src/lib/db.ts everywhere
|
|
238
|
+
import { db } from '@/lib/db';
|
|
239
|
+
const users = await db.select().from(usersTable).where(eq(usersTable.id, id));
|
|
240
|
+
- id: schema_outside_db_dir
|
|
241
|
+
severity: warning
|
|
242
|
+
description: "Defining table schemas outside src/db/schema.ts. drizzle-kit looks for schemas in the path configured in drizzle.config.ts — schemas defined elsewhere are invisible to the migration tool and won't generate migrations."
|
|
243
|
+
bad_example: |
|
|
244
|
+
// ❌ Schema scattered in model files — drizzle-kit can't find it
|
|
245
|
+
// src/models/user.model.ts
|
|
246
|
+
export const users = pgTable('users', { id: text('id') });
|
|
247
|
+
// drizzle-kit generate → no migration generated for this table
|
|
248
|
+
good_example: |
|
|
249
|
+
// ✓ All schemas in src/db/schema.ts — drizzle-kit finds them all
|
|
250
|
+
// drizzle.config.ts: schema: './src/db/schema.ts'
|
|
251
|
+
export const users = pgTable('users', { id: uuid('id').primaryKey() });
|
|
252
|
+
- id: manual_type_interfaces
|
|
253
|
+
severity: warning
|
|
254
|
+
description: "Writing TypeScript interfaces for database row types by hand instead of using `typeof table.$inferSelect`. When the schema changes, the manual interface goes stale — the database returns columns the interface doesn't know about, or the interface references columns that no longer exist."
|
|
255
|
+
bad_example: |
|
|
256
|
+
// ❌ Manual type — gets out of sync when schema changes
|
|
257
|
+
interface User {
|
|
258
|
+
id: string;
|
|
259
|
+
email: string;
|
|
260
|
+
name: string; // What if 'name' is renamed to 'fullName' in the schema?
|
|
261
|
+
}
|
|
262
|
+
good_example: |
|
|
263
|
+
// ✓ Inferred type — always in sync with schema.ts
|
|
264
|
+
import { users } from '@/db/schema';
|
|
265
|
+
export type User = typeof users.$inferSelect;
|
|
266
|
+
export type NewUser = typeof users.$inferInsert;
|
|
267
|
+
- id: push_in_production
|
|
268
|
+
severity: critical
|
|
269
|
+
description: "Using `npx drizzle-kit push` in production environments. `push` modifies the database directly without creating a migration file — there is no migration history, no rollback path, and no CI/CD audit trail. Use `push` only for local prototyping; use `migrate` in all deployed environments."
|
|
270
|
+
bad_example: |
|
|
271
|
+
# ❌ In production CI/CD pipeline — no migration history
|
|
272
|
+
npx drizzle-kit push
|
|
273
|
+
# Database changed but no migration file created — next deploy may push again
|
|
274
|
+
good_example: |
|
|
275
|
+
# ✓ Production: apply versioned migrations from the drizzle/ directory
|
|
276
|
+
npx drizzle-kit migrate
|
|
277
|
+
# Each migration is a timestamped SQL file with a checksum — safe to replay
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
schema_version: "2.0.0"
|
|
2
|
+
id: mongoose
|
|
3
|
+
name: "Mongoose"
|
|
4
|
+
version: "2.0.0"
|
|
5
|
+
description: "Mongoose ODM for MongoDB with typed schemas, single connection, model-layer queries with .lean(), and index-backed query performance."
|
|
6
|
+
category: pattern
|
|
7
|
+
language: javascript
|
|
8
|
+
frameworks:
|
|
9
|
+
- mongoose
|
|
10
|
+
dependencies:
|
|
11
|
+
none:
|
|
12
|
+
- prisma
|
|
13
|
+
- drizzle-orm
|
|
14
|
+
- "@prisma/client"
|
|
15
|
+
detection:
|
|
16
|
+
dependencies:
|
|
17
|
+
any:
|
|
18
|
+
- mongoose
|
|
19
|
+
source_indicators:
|
|
20
|
+
- "mongoose.connect("
|
|
21
|
+
- "new Schema("
|
|
22
|
+
- "model("
|
|
23
|
+
- "mongoose.model("
|
|
24
|
+
- "from 'mongoose'"
|
|
25
|
+
structure:
|
|
26
|
+
required_dirs:
|
|
27
|
+
- path: src/models
|
|
28
|
+
purpose: "Mongoose schema and model definitions — one file per collection (e.g., user.model.ts, post.model.ts). Each file exports the Mongoose model and the TypeScript interface that describes the document shape. Models are the only layer that calls mongoose.model() and defines Schema instances."
|
|
29
|
+
- path: src/lib
|
|
30
|
+
purpose: "Database connection singleton in db.ts — the only place that calls mongoose.connect(). This module is imported once (in the app entry point or Next.js instrumentation) and handles reconnect logic. Never call mongoose.connect() in route handlers or models."
|
|
31
|
+
recommended_dirs:
|
|
32
|
+
- path: src/repositories
|
|
33
|
+
purpose: "Data access layer — one file per model (e.g., user.repository.ts) containing all queries for that collection. Repository functions call Model.find(), Model.findById(), etc. and return typed lean documents. Services call repositories — never the Mongoose Model directly."
|
|
34
|
+
- path: src/types
|
|
35
|
+
purpose: "Shared TypeScript interfaces for document types (HydratedDocument<User>, lean User). Keeps model files focused on schema definition and separates type definitions for reuse across layers."
|
|
36
|
+
separation:
|
|
37
|
+
rules:
|
|
38
|
+
- concern: schema_definition
|
|
39
|
+
belongs_in: src/models
|
|
40
|
+
rule_text: "Define all Mongoose schemas in src/models/ with explicit types matching the TypeScript interface. Always include { timestamps: true } in schema options — Mongoose then manages createdAt and updatedAt automatically. Add index: true or unique: true to fields you query frequently."
|
|
41
|
+
example: |
|
|
42
|
+
// src/models/user.model.ts
|
|
43
|
+
import { Schema, model, type Document } from 'mongoose';
|
|
44
|
+
|
|
45
|
+
export interface IUser {
|
|
46
|
+
email: string;
|
|
47
|
+
name: string;
|
|
48
|
+
role: 'user' | 'admin';
|
|
49
|
+
profileId?: string;
|
|
50
|
+
createdAt: Date; // added by { timestamps: true }
|
|
51
|
+
updatedAt: Date;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const userSchema = new Schema<IUser>(
|
|
55
|
+
{
|
|
56
|
+
email: { type: String, required: true, unique: true, lowercase: true, trim: true },
|
|
57
|
+
name: { type: String, required: true, trim: true },
|
|
58
|
+
role: { type: String, enum: ['user', 'admin'], default: 'user' },
|
|
59
|
+
profileId: { type: String, index: true }, // indexed for fast lookup
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
timestamps: true, // auto-manages createdAt and updatedAt
|
|
63
|
+
toJSON: { virtuals: true }, // include virtuals in JSON output
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
export const User = model<IUser>('User', userSchema);
|
|
68
|
+
indicators:
|
|
69
|
+
- "new Schema("
|
|
70
|
+
- "mongoose.model("
|
|
71
|
+
- "model<I"
|
|
72
|
+
- "timestamps: true"
|
|
73
|
+
- concern: single_connection
|
|
74
|
+
belongs_in: src/lib
|
|
75
|
+
rule_text: "Call mongoose.connect() exactly once in src/lib/db.ts using the cached connection pattern. In Next.js, the connection is shared across hot reloads — without caching the connection promise, each module reload creates a new connection to MongoDB until the pool limit is hit."
|
|
76
|
+
example: |
|
|
77
|
+
// src/lib/db.ts
|
|
78
|
+
import mongoose from 'mongoose';
|
|
79
|
+
|
|
80
|
+
declare global {
|
|
81
|
+
// eslint-disable-next-line no-var
|
|
82
|
+
var mongoose: { conn: typeof import('mongoose') | null; promise: Promise<typeof import('mongoose')> | null };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let cached = global.mongoose ?? { conn: null, promise: null };
|
|
86
|
+
if (!global.mongoose) global.mongoose = cached;
|
|
87
|
+
|
|
88
|
+
export async function connectDB() {
|
|
89
|
+
if (cached.conn) return cached.conn;
|
|
90
|
+
if (!cached.promise) {
|
|
91
|
+
cached.promise = mongoose.connect(process.env.MONGODB_URI!, {
|
|
92
|
+
bufferCommands: false,
|
|
93
|
+
maxPoolSize: process.env.NODE_ENV === 'production' ? 10 : 2,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
cached.conn = await cached.promise;
|
|
97
|
+
return cached.conn;
|
|
98
|
+
}
|
|
99
|
+
anti_indicators:
|
|
100
|
+
- "mongoose.connect("
|
|
101
|
+
- concern: query_performance
|
|
102
|
+
belongs_in: src/repositories
|
|
103
|
+
rule_text: "Use .lean() on read queries that don't need Mongoose document methods (.save(), virtuals, middleware). lean() returns plain JavaScript objects instead of full Mongoose documents — 2-10x faster for large result sets. Use .select() to limit returned fields and .limit() to cap result size."
|
|
104
|
+
example: |
|
|
105
|
+
// src/repositories/user.repository.ts
|
|
106
|
+
import { connectDB } from '@/lib/db';
|
|
107
|
+
import { User, type IUser } from '@/models/user.model';
|
|
108
|
+
|
|
109
|
+
// lean() + select(): fast, minimal-payload read
|
|
110
|
+
export async function listUsers(limit = 50): Promise<Partial<IUser>[]> {
|
|
111
|
+
await connectDB();
|
|
112
|
+
return User.find({ role: 'user' })
|
|
113
|
+
.select('email name createdAt') // only the fields callers need
|
|
114
|
+
.sort({ createdAt: -1 })
|
|
115
|
+
.limit(limit)
|
|
116
|
+
.lean(); // plain objects — no Mongoose overhead
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// No .lean() when mutation is needed (need .save())
|
|
120
|
+
export async function findUserById(id: string) {
|
|
121
|
+
await connectDB();
|
|
122
|
+
return User.findById(id); // returns HydratedDocument<IUser>
|
|
123
|
+
}
|
|
124
|
+
indicators:
|
|
125
|
+
- ".lean()"
|
|
126
|
+
- ".find({"
|
|
127
|
+
- ".sort("
|
|
128
|
+
- ".select("
|
|
129
|
+
- ".limit("
|
|
130
|
+
- concern: population
|
|
131
|
+
belongs_in: src/repositories
|
|
132
|
+
rule_text: "Use .populate() to fetch referenced documents in a single aggregated query instead of manually fetching references in a loop. Pre-populate only the fields the caller needs using the second argument or select option."
|
|
133
|
+
example: |
|
|
134
|
+
// src/repositories/post.repository.ts
|
|
135
|
+
export async function listPostsWithAuthors(limit = 20) {
|
|
136
|
+
await connectDB();
|
|
137
|
+
return Post.find({ publishedAt: { $ne: null } })
|
|
138
|
+
.populate<{ author: IUser }>('author', 'name email') // only name + email from User
|
|
139
|
+
.sort({ publishedAt: -1 })
|
|
140
|
+
.limit(limit)
|
|
141
|
+
.lean();
|
|
142
|
+
}
|
|
143
|
+
indicators:
|
|
144
|
+
- ".populate("
|
|
145
|
+
- concern: transactions
|
|
146
|
+
belongs_in: src/repositories
|
|
147
|
+
rule_text: "Use Mongoose sessions and transactions for operations that span multiple collections and must be atomic. Requires a MongoDB replica set (available on Atlas M10+ or local replica set). Pass the session to every operation that participates in the transaction."
|
|
148
|
+
example: |
|
|
149
|
+
// src/repositories/transfer.repository.ts
|
|
150
|
+
import mongoose from 'mongoose';
|
|
151
|
+
import { Account } from '@/models/account.model';
|
|
152
|
+
import { Transaction } from '@/models/transaction.model';
|
|
153
|
+
|
|
154
|
+
export async function transferFunds(fromId: string, toId: string, amount: number) {
|
|
155
|
+
const session = await mongoose.startSession();
|
|
156
|
+
session.startTransaction();
|
|
157
|
+
try {
|
|
158
|
+
await Account.findByIdAndUpdate(fromId, { $inc: { balance: -amount } }, { session });
|
|
159
|
+
await Account.findByIdAndUpdate(toId, { $inc: { balance: amount } }, { session });
|
|
160
|
+
await Transaction.create([{ fromId, toId, amount }], { session });
|
|
161
|
+
await session.commitTransaction();
|
|
162
|
+
} catch (err) {
|
|
163
|
+
await session.abortTransaction();
|
|
164
|
+
throw err;
|
|
165
|
+
} finally {
|
|
166
|
+
session.endSession();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
indicators:
|
|
170
|
+
- "mongoose.startSession"
|
|
171
|
+
- "startTransaction"
|
|
172
|
+
- "commitTransaction"
|
|
173
|
+
- "abortTransaction"
|
|
174
|
+
patterns:
|
|
175
|
+
data_flow:
|
|
176
|
+
direction: "API Route/Server Action → Service → Repository → Mongoose Model → MongoDB"
|
|
177
|
+
rules:
|
|
178
|
+
- "src/lib/db.ts connectDB() is called before every repository operation — safely no-ops if already connected."
|
|
179
|
+
- "Repositories return plain JavaScript objects (via .lean()) or HydratedDocument only when mutation is needed."
|
|
180
|
+
- "Services call repository functions — never import Mongoose Models directly."
|
|
181
|
+
- "Use .populate() for relations — never loop with findById() to fetch referenced documents."
|
|
182
|
+
- "All queries on frequently-filtered fields have matching index: true in the schema."
|
|
183
|
+
error_handling:
|
|
184
|
+
recommended: "Mongoose validation errors are instanceof mongoose.Error.ValidationError — catch separately from MongoDB driver errors. For duplicate key errors, check err.code === 11000 (MongoDB duplicate key)."
|
|
185
|
+
naming:
|
|
186
|
+
models: "src/models/[resource].model.ts — e.g. user.model.ts exports User model + IUser interface"
|
|
187
|
+
connection: "src/lib/db.ts — exports connectDB() using cached connection pattern"
|
|
188
|
+
repositories: "src/repositories/[resource].repository.ts — e.g. user.repository.ts"
|
|
189
|
+
anti_patterns:
|
|
190
|
+
- id: multiple_connections
|
|
191
|
+
severity: critical
|
|
192
|
+
description: "Calling mongoose.connect() in individual model files, route handlers, or repository functions. In serverless environments, each function invocation recreates the connection — MongoDB Atlas free-tier clusters hit connection limits instantly."
|
|
193
|
+
bad_example: |
|
|
194
|
+
// ❌ mongoose.connect() in a route handler — new connection per request
|
|
195
|
+
export async function GET() {
|
|
196
|
+
await mongoose.connect(process.env.MONGODB_URI!); // new connection every request!
|
|
197
|
+
const users = await User.find().lean();
|
|
198
|
+
return Response.json(users);
|
|
199
|
+
}
|
|
200
|
+
good_example: |
|
|
201
|
+
// ✓ Call connectDB() which uses the cached singleton
|
|
202
|
+
import { connectDB } from '@/lib/db';
|
|
203
|
+
export async function GET() {
|
|
204
|
+
await connectDB(); // no-op if already connected
|
|
205
|
+
const users = await User.find().lean();
|
|
206
|
+
return Response.json(users);
|
|
207
|
+
}
|
|
208
|
+
- id: schema_in_route
|
|
209
|
+
severity: critical
|
|
210
|
+
description: "Defining Mongoose schemas inside route handlers or service functions — the schema and model are recreated on every call. In Next.js, this also triggers a `Cannot overwrite model once compiled` error after HMR because the model name is already registered."
|
|
211
|
+
bad_example: |
|
|
212
|
+
// ❌ Schema defined inside a route handler — recreated every call
|
|
213
|
+
export async function GET() {
|
|
214
|
+
const UserSchema = new Schema({ email: String }); // recreated every request
|
|
215
|
+
const User = model('User', UserSchema); // throws on second call: model already registered
|
|
216
|
+
return Response.json(await User.find());
|
|
217
|
+
}
|
|
218
|
+
good_example: |
|
|
219
|
+
// ✓ Schema defined once in src/models/user.model.ts
|
|
220
|
+
// import the model from there in all route handlers and repositories
|
|
221
|
+
import { User } from '@/models/user.model';
|
|
222
|
+
- id: populate_n_plus_one
|
|
223
|
+
severity: warning
|
|
224
|
+
description: "Fetching documents with references and then manually looping to fetch each referenced document. This creates N+1 database round trips — one query for the list, then one per document for its reference."
|
|
225
|
+
bad_example: |
|
|
226
|
+
// ❌ N+1: 1 query for posts + 1 query per post for its author
|
|
227
|
+
const posts = await Post.find({ publishedAt: { $ne: null } }).lean();
|
|
228
|
+
for (const post of posts) {
|
|
229
|
+
post.author = await User.findById(post.authorId).lean(); // N separate queries
|
|
230
|
+
}
|
|
231
|
+
good_example: |
|
|
232
|
+
// ✓ Single aggregated query via populate
|
|
233
|
+
const posts = await Post.find({ publishedAt: { $ne: null } })
|
|
234
|
+
.populate('author', 'name email') // single JOIN-like query
|
|
235
|
+
.lean();
|
|
236
|
+
- id: missing_schema_index
|
|
237
|
+
severity: warning
|
|
238
|
+
description: "Querying a field frequently without adding index: true (or a compound index) to the schema. Without an index, MongoDB scans every document in the collection on each query — O(n) queries that degrade linearly as data grows."
|
|
239
|
+
bad_example: |
|
|
240
|
+
// ❌ userId queried often but no index — full collection scan on every query
|
|
241
|
+
const userSchema = new Schema({
|
|
242
|
+
email: String,
|
|
243
|
+
userId: String, // frequently queried but not indexed
|
|
244
|
+
}, { timestamps: true });
|
|
245
|
+
|
|
246
|
+
// In repository:
|
|
247
|
+
User.find({ userId }) // COLLSCAN on 1M documents = slow
|
|
248
|
+
good_example: |
|
|
249
|
+
// ✓ Add index to frequently-queried fields
|
|
250
|
+
const userSchema = new Schema({
|
|
251
|
+
email: { type: String, unique: true }, // unique implies index
|
|
252
|
+
userId: { type: String, index: true }, // explicit index for fast lookup
|
|
253
|
+
}, { timestamps: true });
|
|
254
|
+
- id: schema_without_timestamps
|
|
255
|
+
severity: warning
|
|
256
|
+
description: "Defining schemas without `{ timestamps: true }` and manually managing createdAt/updatedAt fields. Manual timestamp management is error-prone — it's easy to forget to update updatedAt on every modification."
|
|
257
|
+
bad_example: |
|
|
258
|
+
// ❌ Manual timestamps — easy to forget updatedAt on updates
|
|
259
|
+
const postSchema = new Schema({
|
|
260
|
+
title: String,
|
|
261
|
+
content: String,
|
|
262
|
+
createdAt: { type: Date, default: Date.now }, // missing from updates
|
|
263
|
+
updatedAt: Date, // must manually set on every save
|
|
264
|
+
});
|
|
265
|
+
good_example: |
|
|
266
|
+
// ✓ Mongoose manages both createdAt and updatedAt automatically
|
|
267
|
+
const postSchema = new Schema(
|
|
268
|
+
{
|
|
269
|
+
title: { type: String, required: true },
|
|
270
|
+
content: String,
|
|
271
|
+
},
|
|
272
|
+
{ timestamps: true } // auto-sets createdAt and updatedAt
|
|
273
|
+
);
|
|
274
|
+
- id: excessive_lean
|
|
275
|
+
severity: warning
|
|
276
|
+
description: "Using .lean() on queries where you then call Mongoose document methods (.save(), .validate(), middleware hooks). .lean() returns plain JavaScript objects — calling .save() on them throws at runtime."
|
|
277
|
+
bad_example: |
|
|
278
|
+
// ❌ .lean() then .save() — TypeError at runtime
|
|
279
|
+
const user = await User.findById(id).lean(); // plain object, not a Mongoose doc
|
|
280
|
+
user.name = 'New Name';
|
|
281
|
+
await user.save(); // TypeError: user.save is not a function
|
|
282
|
+
good_example: |
|
|
283
|
+
// ✓ Skip .lean() when you need to mutate and save
|
|
284
|
+
const user = await User.findById(id); // HydratedDocument<IUser>
|
|
285
|
+
if (!user) throw new Error('User not found');
|
|
286
|
+
user.name = 'New Name';
|
|
287
|
+
await user.save(); // works — Mongoose document method
|
|
288
|
+
|
|
289
|
+
// ✓ Or use findByIdAndUpdate for simpler mutations
|
|
290
|
+
await User.findByIdAndUpdate(id, { name: 'New Name' }, { new: true });
|