WIP Serverless with infra & drizzle start files.

This commit is contained in:
Patrick Fic
2026-01-13 17:35:25 -08:00
parent 21c132642b
commit 66fcaaf8f4
10 changed files with 2724 additions and 251 deletions

View File

@@ -7,4 +7,4 @@ out
.serverless
# Serverless folder (has its own config)
serverless/
#serverless/

View File

@@ -53,6 +53,97 @@ sls deploy --stage dev
sls deploy --stage prod
```
## RDS + Drizzle (Postgres)
This project provisions a **private** Postgres RDS instance and the networking needed for Lambdas to reach it.
### What gets created
Defined in `serverless.yml`:
- A dedicated VPC with 2 public subnets + 2 private subnets
- A NAT Gateway for private-subnet egress
- Security groups:
- Lambda SG (egress all)
- RDS SG (allows inbound `5432` only from the Lambda SG)
- An RDS Postgres instance:
- `DeletionProtection: true`
- `DeletionPolicy: Snapshot` / `UpdateReplacePolicy: Snapshot`
- `AutoMinorVersionUpgrade: true` (minor updates)
- Not publicly accessible
- An RDS Proxy in the same VPC (Lambdas connect to the proxy, proxy connects to RDS)
- A Secrets Manager secret for the RDS master user (generated password)
Note: RDS + NAT Gateway incur AWS costs.
### Drizzle files
- Schema: `src/db/schema/**/*.ts`
- Drizzle config: `drizzle.config.ts`
- Generated migrations (SQL): `drizzle/` (packaged for Lambda)
### Install deps
```bash
cd serverless
npm install
```
### Generate a migration (no DB connection required)
Drizzle can generate SQL migrations by diffing your schema files.
```bash
cd serverless
npm run db:generate
```
This writes SQL into `serverless/drizzle/`.
### Deploy the database
```bash
cd serverless
sls deploy --stage dev
```
RDS creation can take several minutes.
### Apply migrations (recommended: run inside AWS)
Because the RDS instance is in private subnets, it is not directly reachable from your laptop by default.
The repo includes an internal migration Lambda (`dbMigrate`) that runs in the same VPC as the database.
```bash
cd serverless
sls invoke -f dbMigrate --stage dev
```
### Sanity check connectivity
```bash
cd serverless
sls invoke -f dbPing --stage dev
```
### Local migrations (optional)
If you set up network access to the private RDS instance (e.g., via Client VPN or an SSM tunnel through a bastion host), you can run Drizzle migrations locally:
```bash
export DATABASE_URL='postgres://USER:PASSWORD@HOST:5432/esdp'
npm run db:migrate
```
### Deletion protection behavior
- `sls remove` will **not** be able to delete the DB instance while deletion protection is enabled.
- To intentionally remove it later, you must first disable deletion protection in `serverless.yml` and redeploy, then remove the stack.
### Stage-specific DB name
The Postgres database name comes from the stage param `db_name` in `serverless.yml` (e.g. `esdpdev`, `esdpalpha`, `esdpbeta`, `esdpprod`).
## Local Development
```bash

View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema/**/*.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL ?? '',
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,8 @@
"format": "prettier --write 'src/**/*.ts'",
"format:check": "prettier --check 'src/**/*.ts'",
"type-check": "tsc --noEmit",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
@@ -17,9 +19,11 @@
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.965.0",
"@aws-sdk/client-s3": "^3.965.0",
"@aws-sdk/s3-request-presigner": "^3.965.0",
"axios": "^1.13.2",
"drizzle-orm": "^0.44.5",
"form-data": "^4.0.1"
},
"devDependencies": {
@@ -28,6 +32,7 @@
"@types/node": "^25.0.8",
"@typescript-eslint/eslint-plugin": "^8.53.0",
"@typescript-eslint/parser": "^8.53.0",
"drizzle-kit": "^0.31.5",
"esbuild": "^0.27.2",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
@@ -35,6 +40,7 @@
"prettier": "^3.7.4",
"serverless-esbuild": "^1.57.0",
"ts-node": "^10.9.2",
"pg": "^8.16.3",
"typescript": "^5.9.3"
}
}

View File

@@ -1,46 +1,54 @@
service: esdp-api
app: esdp-api-app
frameworkVersion: "4"
frameworkVersion: '4'
package:
patterns:
- drizzle/**
stages:
prod:
# Enables observability in the prod stage
observability: true
prod:
# Enables observability in the prod stage
observability: true
# Sepcify parameter values to be used in the prod stage
params:
es_endpoint: https://insurtechtoolkit.com
domain: es.imex.online
es_user: Imex2
es_password: Patrick
beta:
# Enables observability in the prod stage
observability: false
# Sepcify parameter values to be used in the prod stage
params:
es_endpoint: https://insurtechtoolkit.com
domain: es.imex.online
es_user: Imex2
es_password: Patrick
db_name: esdpprod
beta:
# Enables observability in the prod stage
observability: false
# Sepcify parameter values to be used in the prod stage
params:
es_endpoint: https://4284-79073.el-alt.com
domain: beta.es.imex.online
es_user: Imex2
es_password: Patrick
alpha:
# Enables observability in the prod stage
observability: false
# Sepcify parameter values to be used in the prod stage
params:
es_endpoint: https://4284-79287.el-alt.com
domain: alpha.es.imex.online
es_user: Imex2
es_password: Patrick
dev:
# Enables observability in the prod stage
observability: false
# Sepcify parameter values to be used in the prod stage
params:
es_endpoint: https://4284-79287.el-alt.com
domain: dev.es.imex.online
es_user: Imex2
es_password: Patrick
# Sepcify parameter values to be used in the prod stage
params:
es_endpoint: https://4284-79073.el-alt.com
domain: beta.es.imex.online
es_user: Imex2
es_password: Patrick
db_name: esdpbeta
alpha:
# Enables observability in the prod stage
observability: false
# Sepcify parameter values to be used in the prod stage
params:
es_endpoint: https://4284-79287.el-alt.com
domain: alpha.es.imex.online
es_user: Imex2
es_password: Patrick
db_name: esdpalpha
dev:
# Enables observability in the prod stage
observability: false
# Sepcify parameter values to be used in the prod stage
params:
es_endpoint: https://4284-79287.el-alt.com
domain: dev.es.imex.online
es_user: Imex2
es_password: Patrick
db_name: esdpdev
# params:
# dev:
@@ -53,70 +61,477 @@ stages:
# domain: es.imex.online
provider:
name: aws
runtime: nodejs22.x
region: ca-central-1
domain: ${param:domain}
httpApi: # This creates a cheaper, faster "HTTP API" Gateway
cors: true # Automatically configures CORS
name: aws
runtime: nodejs22.x
region: ca-central-1
domain: ${param:domain}
httpApi: # This creates a cheaper, faster "HTTP API" Gateway
cors: true # Automatically configures CORS
build:
esbuild:
bundle: true
minify: false
sourcemap: true
exclude:
- '@aws-sdk/*'
target: node22
platform: node
esbuild:
bundle: true
minify: false
sourcemap: true
exclude:
- '@aws-sdk/*'
target: node22
platform: node
functions:
vehicleType:
handler: src/handlers/vehicleType.handler
events:
- httpApi:
path: /vehicleType
method: post
scrub:
handler: src/handlers/scrub.handler
environment:
ES_ENDPOINT: ${param:es_endpoint}
ES_USER: ${param:es_user}
ES_PASSWORD: ${param:es_password}
events:
- httpApi:
path: /scrub
method: post
emsupload:
handler: src/handlers/emsupload.handler
environment:
ES_ENDPOINT: ${param:es_endpoint}
UPLOAD_BUCKET_NAME: ${self:service}-uploads-${sls:stage}
iamRoleStatements:
- Effect: Allow
Action:
- s3:PutObject
- s3:PutObjectAcl
- s3:GetObject
Resource:
- arn:aws:s3:::${self:service}-uploads-${sls:stage}/*
events:
- httpApi:
path: /emsupload
method: post
vehicleType:
handler: src/handlers/vehicleType.handler
events:
- httpApi:
path: /vehicleType
method: post
scrub:
handler: src/handlers/scrub.handler
environment:
ES_ENDPOINT: ${param:es_endpoint}
ES_USER: ${param:es_user}
ES_PASSWORD: ${param:es_password}
events:
- httpApi:
path: /scrub
method: post
emsupload:
handler: src/handlers/emsupload.handler
environment:
ES_ENDPOINT: ${param:es_endpoint}
UPLOAD_BUCKET_NAME: ${self:service}-uploads-${sls:stage}
iamRoleStatements:
- Effect: Allow
Action:
- s3:PutObject
- s3:PutObjectAcl
- s3:GetObject
Resource:
- arn:aws:s3:::${self:service}-uploads-${sls:stage}/*
events:
- httpApi:
path: /emsupload
method: post
dbMigrate:
handler: src/handlers/dbMigrate.handler
timeout: 30
memorySize: 512
vpc:
securityGroupIds:
- Ref: LambdaSecurityGroup
subnetIds:
- Ref: PrivateSubnetA
- Ref: PrivateSubnetB
environment:
DB_HOST:
Fn::GetAtt:
- DbProxy
- Endpoint
DB_PORT: 5432
DB_NAME: ${param:db_name}
DB_SECRET_ARN:
Ref: DbSecret
iamRoleStatements:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- Ref: DbSecret
dbPing:
handler: src/handlers/dbPing.handler
timeout: 15
memorySize: 256
vpc:
securityGroupIds:
- Ref: LambdaSecurityGroup
subnetIds:
- Ref: PrivateSubnetA
- Ref: PrivateSubnetB
environment:
DB_HOST:
Fn::GetAtt:
- DbProxy
- Endpoint
DB_PORT: 5432
DB_NAME: ${param:db_name}
DB_SECRET_ARN:
Ref: DbSecret
iamRoleStatements:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- Ref: DbSecret
resources:
Resources:
UploadBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:service}-uploads-${sls:stage}
CorsConfiguration:
CorsRules:
- AllowedOrigins:
- "*"
AllowedMethods:
- PUT
- POST
AllowedHeaders:
- "*"
Resources:
EsdpVpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: ${self:service}-${sls:stage}-vpc
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: ${self:service}-${sls:stage}-igw
VpcGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId:
Ref: EsdpVpc
InternetGatewayId:
Ref: InternetGateway
PublicSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId:
Ref: EsdpVpc
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: ''
CidrBlock: 10.0.0.0/24
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: ${self:service}-${sls:stage}-public-a
PublicSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId:
Ref: EsdpVpc
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: ''
CidrBlock: 10.0.1.0/24
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: ${self:service}-${sls:stage}-public-b
PrivateSubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId:
Ref: EsdpVpc
AvailabilityZone:
Fn::Select:
- 0
- Fn::GetAZs: ''
CidrBlock: 10.0.10.0/24
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: ${self:service}-${sls:stage}-private-a
PrivateSubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId:
Ref: EsdpVpc
AvailabilityZone:
Fn::Select:
- 1
- Fn::GetAZs: ''
CidrBlock: 10.0.11.0/24
MapPublicIpOnLaunch: false
Tags:
- Key: Name
Value: ${self:service}-${sls:stage}-private-b
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: EsdpVpc
Tags:
- Key: Name
Value: ${self:service}-${sls:stage}-public-rt
PublicRoute:
Type: AWS::EC2::Route
DependsOn: VpcGatewayAttachment
Properties:
RouteTableId:
Ref: PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: InternetGateway
PublicSubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId:
Ref: PublicSubnetA
RouteTableId:
Ref: PublicRouteTable
PublicSubnetBRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId:
Ref: PublicSubnetB
RouteTableId:
Ref: PublicRouteTable
NatEip:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId:
Fn::GetAtt:
- NatEip
- AllocationId
SubnetId:
Ref: PublicSubnetA
Tags:
- Key: Name
Value: ${self:service}-${sls:stage}-nat
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: EsdpVpc
Tags:
- Key: Name
Value: ${self:service}-${sls:stage}-private-rt
PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId:
Ref: PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId:
Ref: NatGateway
PrivateSubnetARouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId:
Ref: PrivateSubnetA
RouteTableId:
Ref: PrivateRouteTable
PrivateSubnetBRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId:
Ref: PrivateSubnetB
RouteTableId:
Ref: PrivateRouteTable
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: ${self:service}-${sls:stage} Lambda security group
VpcId:
Ref: EsdpVpc
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: ${self:service}-${sls:stage}-lambda-sg
ProxySecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: ${self:service}-${sls:stage} RDS proxy security group
VpcId:
Ref: EsdpVpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId:
Ref: LambdaSecurityGroup
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp: 70.36.57.88/32
SecurityGroupEgress:
- IpProtocol: -1
CidrIp: 0.0.0.0/0
Tags:
- Key: Name
Value: ${self:service}-${sls:stage}-proxy-sg
RdsSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: ${self:service}-${sls:stage} RDS security group
VpcId:
Ref: EsdpVpc
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
SourceSecurityGroupId:
Ref: ProxySecurityGroup
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp: 70.36.57.88/32
Tags:
- Key: Name
Value: ${self:service}-${sls:stage}-rds-sg
DbSubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: ${self:service}-${sls:stage} DB subnet group
SubnetIds:
- Ref: PrivateSubnetA
- Ref: PrivateSubnetB
- Ref: PublicSubnetA
- Ref: PublicSubnetB
DbSecret:
Type: AWS::SecretsManager::Secret
Properties:
Description: ${self:service}-${sls:stage} RDS master credentials
GenerateSecretString:
SecretStringTemplate: '{"username":"esdp_admin"}'
GenerateStringKey: password
PasswordLength: 32
ExcludeCharacters: '"@/\\'
DbProxyRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- rds.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: ${self:service}-${sls:stage}-db-proxy-secrets
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- Ref: DbSecret
DbProxy:
Type: AWS::RDS::DBProxy
Properties:
DBProxyName: ${self:service}-${sls:stage}-proxy
EngineFamily: POSTGRESQL
IdleClientTimeout: 1800
RequireTLS: true
RoleArn:
Fn::GetAtt:
- DbProxyRole
- Arn
VpcSubnetIds:
- Ref: PrivateSubnetA
- Ref: PrivateSubnetB
VpcSecurityGroupIds:
- Ref: ProxySecurityGroup
Auth:
- AuthScheme: SECRETS
SecretArn:
Ref: DbSecret
IAMAuth: DISABLED
DbProxyTargetGroup:
Type: AWS::RDS::DBProxyTargetGroup
Properties:
DBProxyName:
Ref: DbProxy
TargetGroupName: default
DBInstanceIdentifiers:
- Ref: PostgresDb
ConnectionPoolConfigurationInfo:
MaxConnectionsPercent: 75
MaxIdleConnectionsPercent: 50
ConnectionBorrowTimeout: 120
PostgresDb:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
UpdateReplacePolicy: Snapshot
Properties:
DBInstanceIdentifier: ${self:service}-${sls:stage}-postgres
Engine: postgres
# EngineVersion intentionally omitted so AWS uses the current default/latest for RDS Postgres.
AutoMinorVersionUpgrade: true
DBInstanceClass: db.t4g.micro
AllocatedStorage: 20
StorageType: gp3
StorageEncrypted: true
PubliclyAccessible: true
MultiAZ: false
DBName: ${param:db_name}
BackupRetentionPeriod: 7
CopyTagsToSnapshot: true
DeletionProtection: true
VPCSecurityGroups:
- Ref: RdsSecurityGroup
DBSubnetGroupName:
Ref: DbSubnetGroup
MasterUsername:
Fn::Sub: '{{resolve:secretsmanager:${DbSecret}::username}}'
MasterUserPassword:
Fn::Sub: '{{resolve:secretsmanager:${DbSecret}::password}}'
UploadBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:service}-uploads-${sls:stage}
CorsConfiguration:
CorsRules:
- AllowedOrigins:
- '*'
AllowedMethods:
- PUT
- POST
AllowedHeaders:
- '*'
Outputs:
DbEndpointAddress:
Value:
Fn::GetAtt:
- PostgresDb
- Endpoint.Address
DbEndpointPort:
Value:
Fn::GetAtt:
- PostgresDb
- Endpoint.Port
DbSecretArn:
Value:
Ref: DbSecret
DbProxyEndpoint:
Value:
Fn::GetAtt:
- DbProxy
- Endpoint

View File

@@ -0,0 +1,31 @@
import { AnyPgColumn, boolean, pgTable, text, timestamp, uuid, index } from 'drizzle-orm/pg-core';
export const shops = pgTable('shops', {
id: uuid('id').defaultRandom().primaryKey(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
esApiKey: text('es_api_key').notNull().unique(),
active: boolean('active').notNull().default(true),
});
export const jobs = pgTable(
'jobs',
{
id: uuid('id').defaultRandom().primaryKey(),
shopId: uuid('shopId')
.references((): AnyPgColumn => shops.id)
.notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
clm_no: text('clm_no'),
ciecaid: text('ciecaid'),
},
(table) => [index('clm_no_idx').on(table.clm_no)]
);
export const joblines = pgTable('joblines', {
id: uuid('id').defaultRandom().primaryKey(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
jobId: uuid('jobId')
.references((): AnyPgColumn => jobs.id)
.notNull(),
line_desc: text('line_desc'),
});

View File

@@ -0,0 +1,22 @@
import { APIGatewayProxyResult } from 'aws-lambda';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { getDb } from '../lib/db';
export const handler = async (): Promise<APIGatewayProxyResult> => {
try {
const db = await getDb();
await migrate(db, { migrationsFolder: 'drizzle' });
return {
statusCode: 200,
body: JSON.stringify({ success: true, message: 'Migrations applied.' }),
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
statusCode: 500,
body: JSON.stringify({ success: false, error: errorMessage }),
};
}
};

View File

@@ -0,0 +1,23 @@
import { APIGatewayProxyResult } from 'aws-lambda';
import { sql } from 'drizzle-orm';
import { getDb } from '../lib/db';
export const handler = async (): Promise<APIGatewayProxyResult> => {
try {
const db = await getDb();
const result = await db.execute(sql`select 1 as ok`);
return {
statusCode: 200,
body: JSON.stringify({ success: true, result }),
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
statusCode: 500,
body: JSON.stringify({ success: false, error: errorMessage }),
};
}
};

View File

@@ -1,2 +1,75 @@
// Placeholder for database utilities
// Export database connection and helper functions here
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
import { drizzle, NodePgDatabase } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from '../db/schema';
type DbSecret = {
username: string;
password: string;
};
let cachedSecret: DbSecret | undefined;
let cachedPool: Pool | undefined;
let cachedDb: NodePgDatabase<typeof schema> | undefined;
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`Missing required env var: ${name}`);
}
return value;
}
async function getDbSecret(): Promise<DbSecret> {
if (cachedSecret) return cachedSecret;
const secretArn = requireEnv('DB_SECRET_ARN');
const client = new SecretsManagerClient({});
const result = await client.send(
new GetSecretValueCommand({
SecretId: secretArn,
}),
);
if (!result.SecretString) {
throw new Error('SecretString was empty for DB_SECRET_ARN');
}
const parsed = JSON.parse(result.SecretString) as Partial<DbSecret>;
if (!parsed.username || !parsed.password) {
throw new Error('DB secret missing username/password');
}
cachedSecret = { username: parsed.username, password: parsed.password };
return cachedSecret;
}
export async function getPool(): Promise<Pool> {
if (cachedPool) return cachedPool;
const host = requireEnv('DB_HOST');
const port = Number.parseInt(requireEnv('DB_PORT'), 10);
const database = requireEnv('DB_NAME');
const { username: user, password } = await getDbSecret();
cachedPool = new Pool({
host,
port,
database,
user,
password,
max: 5,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 10_000,
});
return cachedPool;
}
export async function getDb(): Promise<NodePgDatabase<typeof schema>> {
if (cachedDb) return cachedDb;
const pool = await getPool();
cachedDb = drizzle(pool, { schema });
return cachedDb;
}