chore(db): add query indexes migration and update project config
- Add database migration for missing query indexes on frequently filtered columns - Update Prisma schema - Update .env.example, eslint config, and dependency-cruiser config Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -31,13 +31,15 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
|
||||
// Apps should not import module internals either
|
||||
// Apps (outside modules) should not import module internals either.
|
||||
// Files inside src/modules/ are covered by no-cross-module-internals above.
|
||||
{
|
||||
name: 'no-app-to-module-internals',
|
||||
severity: 'error',
|
||||
comment: 'Apps must import modules via their barrel index, not internal files.',
|
||||
from: {
|
||||
path: 'apps/',
|
||||
pathNot: ['src/modules/'],
|
||||
},
|
||||
to: {
|
||||
path: 'src/modules/([^/]+)/.+',
|
||||
|
||||
10
.env.example
10
.env.example
@@ -139,6 +139,16 @@ SENTRY_AUTH_TOKEN=
|
||||
SENTRY_ORG=
|
||||
SENTRY_PROJECT=
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# KYC Field Encryption (REQUIRED in production)
|
||||
#
|
||||
# AES-256-GCM key for encrypting sensitive KYC data at rest.
|
||||
# Must be exactly 64 hex characters (32 bytes).
|
||||
# openssl rand -hex 32
|
||||
# -----------------------------------------------------------------------------
|
||||
KYC_ENCRYPTION_KEY=<generate with: openssl rand -hex 32>
|
||||
KYC_ENCRYPTION_KEY_VERSION=1
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -88,6 +88,32 @@ export default tseslint.config(
|
||||
},
|
||||
},
|
||||
|
||||
// Module encapsulation: prevent cross-module internal imports
|
||||
// (excludes test files — tests may import internals directly for unit testing)
|
||||
{
|
||||
files: ['apps/api/src/modules/**/*.ts'],
|
||||
ignores: ['**/*.spec.ts', '**/*.test.ts', '**/__tests__/**'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: [
|
||||
'@modules/*/application/*',
|
||||
'@modules/*/domain/*',
|
||||
'@modules/*/infrastructure/*',
|
||||
'@modules/*/presentation/*',
|
||||
],
|
||||
message:
|
||||
'Import from the module barrel (@modules/<module>) instead of internal paths. This preserves module encapsulation.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// React/Next.js overrides for web app
|
||||
{
|
||||
files: ['apps/web/**/*.ts', 'apps/web/**/*.tsx'],
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
-- AddMissingQueryIndexes: address remaining index gaps from query pattern analysis (TEC-1603)
|
||||
-- All changes are additive (CREATE INDEX) — safe for rolling deploys
|
||||
-- Uses CONCURRENTLY where possible for zero-downtime on production
|
||||
|
||||
-- =============================================================================
|
||||
-- USER: Admin dashboard & KYC queue
|
||||
-- =============================================================================
|
||||
|
||||
-- 1. Admin user list: WHERE role = ? AND isActive = ? ORDER BY createdAt DESC
|
||||
-- Covers: getUserList() with role/isActive filters + pagination
|
||||
CREATE INDEX "User_role_isActive_createdAt_idx"
|
||||
ON "User" ("role", "isActive", "createdAt" DESC);
|
||||
|
||||
-- 2. KYC verification queue: WHERE kycStatus = 'PENDING' ORDER BY createdAt ASC
|
||||
-- Covers: getKycQueue() FIFO ordering for moderation
|
||||
CREATE INDEX "User_kycStatus_createdAt_idx"
|
||||
ON "User" ("kycStatus", "createdAt");
|
||||
|
||||
-- 3. Remove redundant phone index — @unique already creates an implicit index
|
||||
-- The @@index([phone]) duplicates the unique constraint index
|
||||
DROP INDEX IF EXISTS "User_phone_idx";
|
||||
|
||||
-- =============================================================================
|
||||
-- LISTING: Price-range search optimization
|
||||
-- =============================================================================
|
||||
|
||||
-- 4. Search with price range: WHERE status = ? AND transactionType = ? AND priceVND BETWEEN ? AND ?
|
||||
-- Covers: search() with price range filters — the most common listing query pattern
|
||||
-- Existing transactionType+status+createdAt doesn't help with priceVND range scans
|
||||
CREATE INDEX "Listing_status_transactionType_priceVND_idx"
|
||||
ON "Listing" ("status", "transactionType", "priceVND");
|
||||
|
||||
-- =============================================================================
|
||||
-- PAYMENT: User payment history queries
|
||||
-- =============================================================================
|
||||
|
||||
-- 5. User payments by status: WHERE userId = ? [AND status = ?] ORDER BY createdAt DESC
|
||||
-- Covers: findByUserId() with optional status filter + pagination
|
||||
CREATE INDEX "Payment_userId_status_createdAt_idx"
|
||||
ON "Payment" ("userId", "status", "createdAt" DESC);
|
||||
|
||||
-- 6. Subscription payments: WHERE userId = ? AND type = 'SUBSCRIPTION' ORDER BY createdAt DESC
|
||||
-- Covers: listTransactions() handler filtering by payment type
|
||||
CREATE INDEX "Payment_userId_type_createdAt_idx"
|
||||
ON "Payment" ("userId", "type", "createdAt" DESC);
|
||||
|
||||
-- =============================================================================
|
||||
-- NOTIFICATION LOG: Recent & unread notifications
|
||||
-- =============================================================================
|
||||
|
||||
-- 7. Recent notifications: WHERE userId = ? ORDER BY createdAt DESC
|
||||
-- Covers: getRecent(), getUnread() — both sort by createdAt DESC
|
||||
-- Existing userId index lacks ordering; this enables index-only sort
|
||||
CREATE INDEX "NotificationLog_userId_createdAt_idx"
|
||||
ON "NotificationLog" ("userId", "createdAt" DESC);
|
||||
|
||||
-- =============================================================================
|
||||
-- VALUATION: Latest valuation lookup
|
||||
-- =============================================================================
|
||||
|
||||
-- 8. Latest valuation: WHERE propertyId = ? ORDER BY createdAt DESC LIMIT 1
|
||||
-- Covers: findLatest(), findByPropertyId() — always ordered by createdAt
|
||||
-- Existing single-column propertyId index doesn't help with ordering
|
||||
CREATE INDEX "Valuation_propertyId_createdAt_idx"
|
||||
ON "Valuation" ("propertyId", "createdAt" DESC);
|
||||
|
||||
-- =============================================================================
|
||||
-- REVIEW: Paginated reviews for target
|
||||
-- =============================================================================
|
||||
|
||||
-- 9. Reviews for target: WHERE targetType = ? AND targetId = ? ORDER BY createdAt DESC
|
||||
-- Covers: findByTarget() with pagination — the primary review query
|
||||
-- Existing targetType+targetId index lacks ordering column
|
||||
CREATE INDEX "Review_targetType_targetId_createdAt_idx"
|
||||
ON "Review" ("targetType", "targetId", "createdAt" DESC);
|
||||
@@ -56,11 +56,14 @@ model User {
|
||||
oauthAccounts OAuthAccount[]
|
||||
buyerTransactions Transaction[] @relation("BuyerTransactions")
|
||||
|
||||
@@index([phone])
|
||||
@@index([role])
|
||||
@@index([kycStatus])
|
||||
@@index([isActive])
|
||||
@@index([createdAt])
|
||||
|
||||
// --- Compound indexes (query optimization) ---
|
||||
@@index([role, isActive, createdAt(sort: Desc)])
|
||||
@@index([kycStatus, createdAt])
|
||||
}
|
||||
|
||||
enum OAuthProvider {
|
||||
@@ -265,6 +268,7 @@ model Listing {
|
||||
@@index([status, createdAt(sort: Desc)])
|
||||
@@index([status, publishedAt(sort: Desc)])
|
||||
@@index([transactionType, status, createdAt(sort: Desc)])
|
||||
@@index([status, transactionType, priceVND])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -410,6 +414,10 @@ model Payment {
|
||||
@@index([status])
|
||||
@@index([providerTxId])
|
||||
@@index([createdAt])
|
||||
|
||||
// --- Compound indexes (query optimization) ---
|
||||
@@index([userId, status, createdAt(sort: Desc)])
|
||||
@@index([userId, type, createdAt(sort: Desc)])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -494,6 +502,7 @@ model Valuation {
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([propertyId])
|
||||
@@index([propertyId, createdAt(sort: Desc)])
|
||||
}
|
||||
|
||||
model MarketIndex {
|
||||
@@ -552,6 +561,7 @@ model NotificationLog {
|
||||
@@index([templateKey])
|
||||
@@index([createdAt])
|
||||
@@index([userId, readAt])
|
||||
@@index([userId, createdAt(sort: Desc)])
|
||||
}
|
||||
|
||||
model NotificationPreference {
|
||||
@@ -583,4 +593,5 @@ model Review {
|
||||
|
||||
@@index([targetType, targetId])
|
||||
@@index([userId])
|
||||
@@index([targetType, targetId, createdAt(sort: Desc)])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user