stacktape 3.5.8 → 3.6.0-beta.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/.tsconfig.bun-build.json +1 -0
- package/ai-docs/cli-ref/aws-profile-create.md +22 -0
- package/ai-docs/cli-ref/aws-profile-delete.md +22 -0
- package/ai-docs/cli-ref/aws-profile-list.md +20 -0
- package/ai-docs/cli-ref/aws-profile-update.md +22 -0
- package/ai-docs/cli-ref/bastion-session.md +29 -0
- package/ai-docs/cli-ref/bastion-tunnel.md +30 -0
- package/ai-docs/cli-ref/bucket-sync.md +30 -0
- package/ai-docs/cli-ref/cf-module-update.md +26 -0
- package/ai-docs/cli-ref/cf-rollback.md +28 -0
- package/ai-docs/cli-ref/codebuild-deploy.md +34 -0
- package/ai-docs/cli-ref/compile-template.md +25 -0
- package/ai-docs/cli-ref/container-session.md +30 -0
- package/ai-docs/cli-ref/debug-alarms.md +28 -0
- package/ai-docs/cli-ref/debug-aws-sdk.md +33 -0
- package/ai-docs/cli-ref/debug-container-exec.md +36 -0
- package/ai-docs/cli-ref/debug-dynamodb.md +35 -0
- package/ai-docs/cli-ref/debug-logs.md +34 -0
- package/ai-docs/cli-ref/debug-metrics.md +33 -0
- package/ai-docs/cli-ref/debug-opensearch.md +35 -0
- package/ai-docs/cli-ref/debug-redis.md +36 -0
- package/ai-docs/cli-ref/debug-sql.md +35 -0
- package/ai-docs/cli-ref/defaults-configure.md +29 -0
- package/ai-docs/cli-ref/defaults-list.md +20 -0
- package/ai-docs/cli-ref/delete.md +24 -0
- package/ai-docs/cli-ref/deploy.md +25 -0
- package/ai-docs/cli-ref/deployment-script-run.md +28 -0
- package/ai-docs/cli-ref/dev-stop.md +26 -0
- package/ai-docs/cli-ref/dev.md +45 -0
- package/ai-docs/cli-ref/domain-add.md +26 -0
- package/ai-docs/cli-ref/help.md +18 -0
- package/ai-docs/cli-ref/info-operations.md +22 -0
- package/ai-docs/cli-ref/info-stack.md +30 -0
- package/ai-docs/cli-ref/info-stacks.md +26 -0
- package/ai-docs/cli-ref/info-whoami.md +22 -0
- package/ai-docs/cli-ref/init.md +30 -0
- package/ai-docs/cli-ref/login.md +20 -0
- package/ai-docs/cli-ref/logout.md +18 -0
- package/ai-docs/cli-ref/mcp-add.md +22 -0
- package/ai-docs/cli-ref/mcp.md +20 -0
- package/ai-docs/cli-ref/org-create.md +24 -0
- package/ai-docs/cli-ref/org-delete.md +24 -0
- package/ai-docs/cli-ref/org-list.md +22 -0
- package/ai-docs/cli-ref/package-workloads.md +25 -0
- package/ai-docs/cli-ref/param-get.md +26 -0
- package/ai-docs/cli-ref/preview-changes.md +23 -0
- package/ai-docs/cli-ref/project-create.md +22 -0
- package/ai-docs/cli-ref/projects-list.md +22 -0
- package/ai-docs/cli-ref/rollback.md +28 -0
- package/ai-docs/cli-ref/script-run.md +29 -0
- package/ai-docs/cli-ref/secret-create.md +28 -0
- package/ai-docs/cli-ref/secret-delete.md +26 -0
- package/ai-docs/cli-ref/secret-get.md +26 -0
- package/ai-docs/cli-ref/upgrade.md +20 -0
- package/ai-docs/cli-ref/version.md +18 -0
- package/ai-docs/concept/connecting-resources.md +369 -0
- package/ai-docs/concept/directives.md +371 -0
- package/ai-docs/concept/extending-cloudformation.md +315 -0
- package/ai-docs/concept/overrides-and-transforms.md +352 -0
- package/ai-docs/concept/stages-and-environments.md +347 -0
- package/ai-docs/concept/typescript-config.md +447 -0
- package/ai-docs/concept/yaml-config.md +338 -0
- package/ai-docs/config-ref/_root.md +142 -0
- package/ai-docs/config-ref/application-load-balancer.md +1109 -0
- package/ai-docs/config-ref/astro-web.md +115 -0
- package/ai-docs/config-ref/aws-cdk-construct.md +68 -0
- package/ai-docs/config-ref/bastion.md +93 -0
- package/ai-docs/config-ref/batch-job.md +179 -0
- package/ai-docs/config-ref/bucket.md +348 -0
- package/ai-docs/config-ref/cdn.md +496 -0
- package/ai-docs/config-ref/custom-resource.md +80 -0
- package/ai-docs/config-ref/deployment-script.md +79 -0
- package/ai-docs/config-ref/dynamo-db-table.md +202 -0
- package/ai-docs/config-ref/edge-lambda-function.md +87 -0
- package/ai-docs/config-ref/efs-filesystem.md +72 -0
- package/ai-docs/config-ref/event-bus.md +63 -0
- package/ai-docs/config-ref/function.md +409 -0
- package/ai-docs/config-ref/hosting-bucket.md +171 -0
- package/ai-docs/config-ref/http-api-gateway.md +149 -0
- package/ai-docs/config-ref/http-endpoint.md +92 -0
- package/ai-docs/config-ref/kinesis-stream.md +97 -0
- package/ai-docs/config-ref/mongo-db-atlas-cluster.md +254 -0
- package/ai-docs/config-ref/multi-container-workload.md +399 -0
- package/ai-docs/config-ref/network-load-balancer.md +118 -0
- package/ai-docs/config-ref/nextjs-web.md +147 -0
- package/ai-docs/config-ref/nuxt-web.md +81 -0
- package/ai-docs/config-ref/open-search.md +206 -0
- package/ai-docs/config-ref/private-service.md +75 -0
- package/ai-docs/config-ref/redis-cluster.md +223 -0
- package/ai-docs/config-ref/relational-database.md +525 -0
- package/ai-docs/config-ref/remix-web.md +74 -0
- package/ai-docs/config-ref/sns-topic.md +69 -0
- package/ai-docs/config-ref/solidstart-web.md +75 -0
- package/ai-docs/config-ref/sqs-queue-not-empty.md +405 -0
- package/ai-docs/config-ref/sqs-queue.md +232 -0
- package/ai-docs/config-ref/state-machine.md +235 -0
- package/ai-docs/config-ref/sveltekit-web.md +81 -0
- package/ai-docs/config-ref/tanstack-web.md +75 -0
- package/ai-docs/config-ref/upstash-redis.md +59 -0
- package/ai-docs/config-ref/user-auth-pool.md +876 -0
- package/ai-docs/config-ref/web-app-firewall.md +212 -0
- package/ai-docs/config-ref/web-service.md +178 -0
- package/ai-docs/config-ref/worker-service.md +41 -0
- package/ai-docs/getting-started/console.md +232 -0
- package/ai-docs/getting-started/deployment.md +434 -0
- package/ai-docs/getting-started/dev-mode.md +118 -0
- package/ai-docs/getting-started/how-it-works.md +119 -0
- package/ai-docs/getting-started/intro.md +157 -0
- package/ai-docs/getting-started/using-with-ai.md +228 -0
- package/ai-docs/getting-started/workflow.md +197 -0
- package/ai-docs/index.json +1514 -0
- package/ai-docs/recipe/background-jobs.md +183 -0
- package/ai-docs/recipe/database-migrations.md +240 -0
- package/ai-docs/recipe/graphql-api.md +211 -0
- package/ai-docs/recipe/monorepo-setup.md +183 -0
- package/ai-docs/recipe/nextjs-full-stack.md +188 -0
- package/ai-docs/recipe/rest-api-with-database.md +156 -0
- package/ai-docs/recipe/scheduled-tasks.md +186 -0
- package/ai-docs/recipe/static-website.md +241 -0
- package/ai-docs/troubleshooting/cloudformation-stack-states.md +189 -0
- package/bin/stacktape.js +0 -12
- package/package.json +1 -1
- package/plain.d.ts +309 -54
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
docType: recipe
|
|
3
|
+
title: Background Jobs
|
|
4
|
+
tags:
|
|
5
|
+
- background
|
|
6
|
+
- jobs
|
|
7
|
+
- recipe
|
|
8
|
+
source: docs/_curated-docs/recipes/background-jobs.mdx
|
|
9
|
+
priority: 1
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Background Job Processing
|
|
13
|
+
|
|
14
|
+
Process jobs asynchronously using SQS queues and Lambda functions.
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { defineConfig, LambdaFunction, SqsQueue, HttpApiGateway } from 'stacktape';
|
|
20
|
+
|
|
21
|
+
export default defineConfig(() => {
|
|
22
|
+
// Job queue
|
|
23
|
+
const jobQueue = new SqsQueue({
|
|
24
|
+
visibilityTimeoutSeconds: 300, // 5 minutes to process
|
|
25
|
+
messageRetentionPeriodSeconds: 1209600 // Keep for 14 days
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Job processor
|
|
29
|
+
const jobProcessor = new LambdaFunction({
|
|
30
|
+
packaging: {
|
|
31
|
+
type: 'stacktape-lambda-buildpack',
|
|
32
|
+
properties: {
|
|
33
|
+
entryfilePath: './src/processor.ts'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
timeout: 300, // 5 minute timeout
|
|
37
|
+
memory: 1024,
|
|
38
|
+
events: [
|
|
39
|
+
{
|
|
40
|
+
type: 'sqs',
|
|
41
|
+
properties: {
|
|
42
|
+
sqsQueueName: 'jobQueue',
|
|
43
|
+
batchSize: 10
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// API to enqueue jobs
|
|
50
|
+
const api = new LambdaFunction({
|
|
51
|
+
packaging: {
|
|
52
|
+
type: 'stacktape-lambda-buildpack',
|
|
53
|
+
properties: {
|
|
54
|
+
entryfilePath: './src/api.ts'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
connectTo: [jobQueue]
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const gateway = new HttpApiGateway({
|
|
61
|
+
routes: [{ path: '/jobs', method: 'POST', integration: { type: 'function', properties: { function: api } } }]
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
resources: { jobQueue, jobProcessor, api, gateway }
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API Handler (Enqueue Jobs)
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// src/api.ts
|
|
74
|
+
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
|
|
75
|
+
import { APIGatewayProxyHandlerV2 } from 'aws-lambda';
|
|
76
|
+
|
|
77
|
+
const sqs = new SQSClient({});
|
|
78
|
+
|
|
79
|
+
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
|
|
80
|
+
const body = JSON.parse(event.body || '{}');
|
|
81
|
+
|
|
82
|
+
await sqs.send(
|
|
83
|
+
new SendMessageCommand({
|
|
84
|
+
QueueUrl: process.env.STP_JOBQUEUE_QUEUE_URL,
|
|
85
|
+
MessageBody: JSON.stringify({
|
|
86
|
+
type: body.type,
|
|
87
|
+
payload: body.payload,
|
|
88
|
+
createdAt: new Date().toISOString()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
statusCode: 202,
|
|
95
|
+
body: JSON.stringify({ message: 'Job queued' })
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Job Processor
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// src/processor.ts
|
|
104
|
+
import { SQSHandler } from 'aws-lambda';
|
|
105
|
+
|
|
106
|
+
export const handler: SQSHandler = async (event) => {
|
|
107
|
+
for (const record of event.Records) {
|
|
108
|
+
const job = JSON.parse(record.body);
|
|
109
|
+
|
|
110
|
+
console.log(`Processing job: ${job.type}`);
|
|
111
|
+
|
|
112
|
+
switch (job.type) {
|
|
113
|
+
case 'email':
|
|
114
|
+
await sendEmail(job.payload);
|
|
115
|
+
break;
|
|
116
|
+
case 'resize-image':
|
|
117
|
+
await resizeImage(job.payload);
|
|
118
|
+
break;
|
|
119
|
+
case 'generate-report':
|
|
120
|
+
await generateReport(job.payload);
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
console.warn(`Unknown job type: ${job.type}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
async function sendEmail(payload: any) {
|
|
129
|
+
// Email sending logic
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function resizeImage(payload: any) {
|
|
133
|
+
// Image processing logic
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function generateReport(payload: any) {
|
|
137
|
+
// Report generation logic
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Dead Letter Queue
|
|
142
|
+
|
|
143
|
+
Handle failed jobs:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// Failed jobs go here after 3 retries
|
|
147
|
+
const deadLetterQueue = new SqsQueue({
|
|
148
|
+
messageRetentionPeriodSeconds: 1209600
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const jobQueue = new SqsQueue({
|
|
152
|
+
visibilityTimeoutSeconds: 300,
|
|
153
|
+
redrivePolicy: {
|
|
154
|
+
targetSqsQueueName: 'deadLetterQueue',
|
|
155
|
+
maxReceiveCount: 3 // Retry 3 times before DLQ
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Alert on DLQ messages
|
|
160
|
+
const dlqProcessor = new LambdaFunction({
|
|
161
|
+
packaging: {
|
|
162
|
+
type: 'stacktape-lambda-buildpack',
|
|
163
|
+
properties: {
|
|
164
|
+
entryfilePath: './src/dlq-alert.ts'
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
events: [
|
|
168
|
+
{
|
|
169
|
+
type: 'sqs',
|
|
170
|
+
properties: { sqsQueueName: 'deadLetterQueue' }
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Usage
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# Enqueue a job
|
|
180
|
+
curl -X POST https://your-api.execute-api.us-east-1.amazonaws.com/jobs \
|
|
181
|
+
-H "Content-Type: application/json" \
|
|
182
|
+
-d '{"type": "email", "payload": {"to": "user@example.com", "subject": "Hello"}}'
|
|
183
|
+
```
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
docType: recipe
|
|
3
|
+
title: Database Migrations
|
|
4
|
+
tags:
|
|
5
|
+
- database
|
|
6
|
+
- migrations
|
|
7
|
+
- recipe
|
|
8
|
+
source: docs/_curated-docs/recipes/database-migrations.mdx
|
|
9
|
+
priority: 1
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Database Migrations
|
|
13
|
+
|
|
14
|
+
Run database migrations automatically during deployment.
|
|
15
|
+
|
|
16
|
+
## Using Prisma
|
|
17
|
+
|
|
18
|
+
### Configuration
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import {
|
|
22
|
+
defineConfig,
|
|
23
|
+
LambdaFunction,
|
|
24
|
+
RelationalDatabase,
|
|
25
|
+
RdsEnginePostgres,
|
|
26
|
+
HttpApiGateway,
|
|
27
|
+
$Secret,
|
|
28
|
+
$ResourceParam
|
|
29
|
+
} from 'stacktape';
|
|
30
|
+
|
|
31
|
+
export default defineConfig(({ stage }) => {
|
|
32
|
+
const database = new RelationalDatabase({
|
|
33
|
+
engine: new RdsEnginePostgres({ version: '16' }),
|
|
34
|
+
credentials: {
|
|
35
|
+
masterUserPassword: $Secret(`db-password-${stage}`)
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const api = new LambdaFunction({
|
|
40
|
+
packaging: {
|
|
41
|
+
type: 'stacktape-lambda-buildpack',
|
|
42
|
+
properties: {
|
|
43
|
+
entryfilePath: './src/handler.ts'
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
connectTo: [database]
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const gateway = new HttpApiGateway({
|
|
50
|
+
routes: [{ path: '/{proxy+}', method: '*', integration: { type: 'function', properties: { function: api } } }]
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
hooks: {
|
|
55
|
+
afterDeploy: [{ scriptName: 'migrate' }]
|
|
56
|
+
},
|
|
57
|
+
scripts: {
|
|
58
|
+
migrate: {
|
|
59
|
+
executeCommand: 'npx prisma migrate deploy',
|
|
60
|
+
environment: {
|
|
61
|
+
DATABASE_URL: $ResourceParam('database', 'connectionString')
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
// Optional: seed script for development
|
|
65
|
+
seed: {
|
|
66
|
+
executeCommand: 'npx prisma db seed',
|
|
67
|
+
environment: {
|
|
68
|
+
DATABASE_URL: $ResourceParam('database', 'connectionString')
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
resources: { database, api, gateway }
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Prisma Schema
|
|
78
|
+
|
|
79
|
+
```prisma
|
|
80
|
+
// prisma/schema.prisma
|
|
81
|
+
generator client {
|
|
82
|
+
provider = "prisma-client-js"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
datasource db {
|
|
86
|
+
provider = "postgresql"
|
|
87
|
+
url = env("DATABASE_URL")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
model User {
|
|
91
|
+
id Int @id @default(autoincrement())
|
|
92
|
+
email String @unique
|
|
93
|
+
name String?
|
|
94
|
+
posts Post[]
|
|
95
|
+
createdAt DateTime @default(now())
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
model Post {
|
|
99
|
+
id Int @id @default(autoincrement())
|
|
100
|
+
title String
|
|
101
|
+
content String?
|
|
102
|
+
author User @relation(fields: [authorId], references: [id])
|
|
103
|
+
authorId Int
|
|
104
|
+
createdAt DateTime @default(now())
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Creating Migrations
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Development: create a new migration
|
|
112
|
+
npx prisma migrate dev --name add_users_table
|
|
113
|
+
|
|
114
|
+
# This creates: prisma/migrations/20240115_add_users_table/migration.sql
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Using Drizzle
|
|
118
|
+
|
|
119
|
+
### Configuration
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
export default defineConfig(({ stage }) => {
|
|
123
|
+
// ... resources ...
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
hooks: {
|
|
127
|
+
afterDeploy: [{ scriptName: 'migrate' }]
|
|
128
|
+
},
|
|
129
|
+
scripts: {
|
|
130
|
+
migrate: {
|
|
131
|
+
executeCommand: 'npx drizzle-kit push',
|
|
132
|
+
environment: {
|
|
133
|
+
DATABASE_URL: $ResourceParam('database', 'connectionString')
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
resources: { database, api, gateway }
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Using Raw SQL
|
|
143
|
+
|
|
144
|
+
### Migration Script
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// scripts/migrate.ts
|
|
148
|
+
import { Pool } from 'pg';
|
|
149
|
+
import { readdir, readFile } from 'fs/promises';
|
|
150
|
+
import { join } from 'path';
|
|
151
|
+
|
|
152
|
+
const pool = new Pool({
|
|
153
|
+
connectionString: process.env.DATABASE_URL
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
async function migrate() {
|
|
157
|
+
// Create migrations table if not exists
|
|
158
|
+
await pool.query(`
|
|
159
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
160
|
+
id SERIAL PRIMARY KEY,
|
|
161
|
+
name VARCHAR(255) NOT NULL UNIQUE,
|
|
162
|
+
applied_at TIMESTAMP DEFAULT NOW()
|
|
163
|
+
)
|
|
164
|
+
`);
|
|
165
|
+
|
|
166
|
+
// Get applied migrations
|
|
167
|
+
const { rows: applied } = await pool.query('SELECT name FROM _migrations');
|
|
168
|
+
const appliedNames = new Set(applied.map((r) => r.name));
|
|
169
|
+
|
|
170
|
+
// Get migration files
|
|
171
|
+
const migrationsDir = join(__dirname, '../migrations');
|
|
172
|
+
const files = await readdir(migrationsDir);
|
|
173
|
+
const pending = files
|
|
174
|
+
.filter((f) => f.endsWith('.sql'))
|
|
175
|
+
.filter((f) => !appliedNames.has(f))
|
|
176
|
+
.sort();
|
|
177
|
+
|
|
178
|
+
// Apply pending migrations
|
|
179
|
+
for (const file of pending) {
|
|
180
|
+
console.log(`Applying: ${file}`);
|
|
181
|
+
const sql = await readFile(join(migrationsDir, file), 'utf-8');
|
|
182
|
+
|
|
183
|
+
await pool.query('BEGIN');
|
|
184
|
+
try {
|
|
185
|
+
await pool.query(sql);
|
|
186
|
+
await pool.query('INSERT INTO _migrations (name) VALUES ($1)', [file]);
|
|
187
|
+
await pool.query('COMMIT');
|
|
188
|
+
console.log(`Applied: ${file}`);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
await pool.query('ROLLBACK');
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log('Migrations complete');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
migrate()
|
|
199
|
+
.catch(console.error)
|
|
200
|
+
.finally(() => pool.end());
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Stacktape Script
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
scripts: {
|
|
207
|
+
migrate: {
|
|
208
|
+
executeCommand: 'npx ts-node scripts/migrate.ts',
|
|
209
|
+
environment: {
|
|
210
|
+
DATABASE_URL: $ResourceParam('database', 'connectionString')
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Migration Best Practices
|
|
217
|
+
|
|
218
|
+
1. **Always test migrations locally first**
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
stacktape dev --stage dev --region us-east-1
|
|
222
|
+
# Then run migrations against local emulated database
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
2. **Make migrations idempotent when possible**
|
|
226
|
+
|
|
227
|
+
```sql
|
|
228
|
+
CREATE TABLE IF NOT EXISTS users (...);
|
|
229
|
+
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
3. **Use transactions for safety**
|
|
233
|
+
- Prisma and Drizzle handle this automatically
|
|
234
|
+
- For raw SQL, wrap in BEGIN/COMMIT
|
|
235
|
+
|
|
236
|
+
4. **Back up production before migrating**
|
|
237
|
+
```bash
|
|
238
|
+
# Create a snapshot before deploying
|
|
239
|
+
aws rds create-db-snapshot --db-instance-identifier my-db --db-snapshot-identifier pre-migration-backup
|
|
240
|
+
```
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
docType: recipe
|
|
3
|
+
title: GraphQL API
|
|
4
|
+
tags:
|
|
5
|
+
- graphql
|
|
6
|
+
- api
|
|
7
|
+
- recipe
|
|
8
|
+
source: docs/_curated-docs/recipes/graphql-api.mdx
|
|
9
|
+
priority: 1
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# GraphQL API
|
|
13
|
+
|
|
14
|
+
Deploy a GraphQL API using Apollo Server.
|
|
15
|
+
|
|
16
|
+
## Lambda-based (Serverless)
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import {
|
|
20
|
+
defineConfig,
|
|
21
|
+
LambdaFunction,
|
|
22
|
+
RelationalDatabase,
|
|
23
|
+
RdsEnginePostgres,
|
|
24
|
+
HttpApiGateway,
|
|
25
|
+
$Secret
|
|
26
|
+
} from 'stacktape';
|
|
27
|
+
|
|
28
|
+
export default defineConfig(({ stage }) => {
|
|
29
|
+
const database = new RelationalDatabase({
|
|
30
|
+
engine: new RdsEnginePostgres({ version: '16' }),
|
|
31
|
+
credentials: {
|
|
32
|
+
masterUserPassword: $Secret(`db-password-${stage}`)
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const graphql = new LambdaFunction({
|
|
37
|
+
packaging: {
|
|
38
|
+
type: 'stacktape-lambda-buildpack',
|
|
39
|
+
properties: {
|
|
40
|
+
entryfilePath: './src/graphql.ts'
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
timeout: 30,
|
|
44
|
+
memory: 1024,
|
|
45
|
+
connectTo: [database]
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const gateway = new HttpApiGateway({
|
|
49
|
+
routes: [{ path: '/graphql', method: '*', integration: { type: 'function', properties: { function: graphql } } }]
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
resources: { database, graphql, gateway }
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Apollo Server Handler
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// src/graphql.ts
|
|
62
|
+
import { ApolloServer } from '@apollo/server';
|
|
63
|
+
import { startServerAndCreateLambdaHandler, handlers } from '@as-integrations/aws-lambda';
|
|
64
|
+
import { PrismaClient } from '@prisma/client';
|
|
65
|
+
|
|
66
|
+
const prisma = new PrismaClient();
|
|
67
|
+
|
|
68
|
+
const typeDefs = `#graphql
|
|
69
|
+
type User {
|
|
70
|
+
id: ID!
|
|
71
|
+
email: String!
|
|
72
|
+
name: String
|
|
73
|
+
posts: [Post!]!
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type Post {
|
|
77
|
+
id: ID!
|
|
78
|
+
title: String!
|
|
79
|
+
content: String
|
|
80
|
+
author: User!
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type Query {
|
|
84
|
+
users: [User!]!
|
|
85
|
+
user(id: ID!): User
|
|
86
|
+
posts: [Post!]!
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type Mutation {
|
|
90
|
+
createUser(email: String!, name: String): User!
|
|
91
|
+
createPost(title: String!, content: String, authorId: ID!): Post!
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const resolvers = {
|
|
96
|
+
Query: {
|
|
97
|
+
users: () => prisma.user.findMany(),
|
|
98
|
+
user: (_: any, { id }: { id: string }) => prisma.user.findUnique({ where: { id: parseInt(id) } }),
|
|
99
|
+
posts: () => prisma.post.findMany()
|
|
100
|
+
},
|
|
101
|
+
Mutation: {
|
|
102
|
+
createUser: (_: any, { email, name }: { email: string; name?: string }) =>
|
|
103
|
+
prisma.user.create({ data: { email, name } }),
|
|
104
|
+
createPost: (_: any, { title, content, authorId }: { title: string; content?: string; authorId: string }) =>
|
|
105
|
+
prisma.post.create({ data: { title, content, authorId: parseInt(authorId) } })
|
|
106
|
+
},
|
|
107
|
+
User: {
|
|
108
|
+
posts: (user: any) => prisma.post.findMany({ where: { authorId: user.id } })
|
|
109
|
+
},
|
|
110
|
+
Post: {
|
|
111
|
+
author: (post: any) => prisma.user.findUnique({ where: { id: post.authorId } })
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const server = new ApolloServer({
|
|
116
|
+
typeDefs,
|
|
117
|
+
resolvers,
|
|
118
|
+
introspection: process.env.NODE_ENV !== 'production'
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export const handler = startServerAndCreateLambdaHandler(server, handlers.createAPIGatewayProxyEventV2RequestHandler());
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Container-based (For Complex APIs)
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { defineConfig, WebService, RelationalDatabase, RdsEnginePostgres, $Secret } from 'stacktape';
|
|
128
|
+
|
|
129
|
+
export default defineConfig(({ stage }) => {
|
|
130
|
+
const database = new RelationalDatabase({
|
|
131
|
+
engine: new RdsEnginePostgres({ version: '16' }),
|
|
132
|
+
credentials: {
|
|
133
|
+
masterUserPassword: $Secret(`db-password-${stage}`)
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const graphql = new WebService({
|
|
138
|
+
packaging: {
|
|
139
|
+
type: 'stacktape-image-buildpack',
|
|
140
|
+
properties: {
|
|
141
|
+
entryfilePath: './src/server.ts'
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
resources: {
|
|
145
|
+
cpu: 0.5,
|
|
146
|
+
memory: 1024
|
|
147
|
+
},
|
|
148
|
+
connectTo: [database],
|
|
149
|
+
scaling: {
|
|
150
|
+
minInstances: 1,
|
|
151
|
+
maxInstances: 10
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
resources: { database, graphql }
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Container Server
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// src/server.ts
|
|
165
|
+
import { ApolloServer } from '@apollo/server';
|
|
166
|
+
import { expressMiddleware } from '@apollo/server/express4';
|
|
167
|
+
import express from 'express';
|
|
168
|
+
import cors from 'cors';
|
|
169
|
+
|
|
170
|
+
const app = express();
|
|
171
|
+
|
|
172
|
+
const server = new ApolloServer({
|
|
173
|
+
typeDefs,
|
|
174
|
+
resolvers
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await server.start();
|
|
178
|
+
|
|
179
|
+
app.use('/graphql', cors(), express.json(), expressMiddleware(server));
|
|
180
|
+
|
|
181
|
+
app.listen(process.env.PORT || 80, () => {
|
|
182
|
+
console.log('GraphQL server running');
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Dependencies
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"dependencies": {
|
|
191
|
+
"@apollo/server": "^4.0.0",
|
|
192
|
+
"@as-integrations/aws-lambda": "^3.0.0",
|
|
193
|
+
"@prisma/client": "^5.0.0",
|
|
194
|
+
"graphql": "^16.0.0"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Testing
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Query
|
|
203
|
+
curl -X POST https://your-api.execute-api.us-east-1.amazonaws.com/graphql \
|
|
204
|
+
-H "Content-Type: application/json" \
|
|
205
|
+
-d '{"query": "{ users { id email name } }"}'
|
|
206
|
+
|
|
207
|
+
# Mutation
|
|
208
|
+
curl -X POST https://your-api.execute-api.us-east-1.amazonaws.com/graphql \
|
|
209
|
+
-H "Content-Type: application/json" \
|
|
210
|
+
-d '{"query": "mutation { createUser(email: \"test@example.com\", name: \"Test\") { id } }"}'
|
|
211
|
+
```
|