render-create 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/README.md +207 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +45 -0
- package/dist/commands/check.d.ts +8 -0
- package/dist/commands/check.js +96 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.js +1201 -0
- package/dist/commands/sync.d.ts +8 -0
- package/dist/commands/sync.js +126 -0
- package/dist/types.d.ts +246 -0
- package/dist/types.js +4 -0
- package/dist/utils.d.ts +53 -0
- package/dist/utils.js +142 -0
- package/package.json +65 -0
- package/templates/LINTING_SETUP.md +205 -0
- package/templates/README_TEMPLATE.md +68 -0
- package/templates/STYLE_GUIDE.md +241 -0
- package/templates/assets/favicon.png +0 -0
- package/templates/assets/favicon.svg +17 -0
- package/templates/biome.json +43 -0
- package/templates/cursor/rules/drizzle.mdc +165 -0
- package/templates/cursor/rules/fastify.mdc +132 -0
- package/templates/cursor/rules/general.mdc +112 -0
- package/templates/cursor/rules/nextjs.mdc +89 -0
- package/templates/cursor/rules/python.mdc +89 -0
- package/templates/cursor/rules/react.mdc +200 -0
- package/templates/cursor/rules/sqlalchemy.mdc +205 -0
- package/templates/cursor/rules/tailwind.mdc +139 -0
- package/templates/cursor/rules/typescript.mdc +112 -0
- package/templates/cursor/rules/vite.mdc +169 -0
- package/templates/cursor/rules/workflows.mdc +349 -0
- package/templates/docker-compose.example.yml +55 -0
- package/templates/drizzle/db-index.ts +15 -0
- package/templates/drizzle/drizzle.config.ts +10 -0
- package/templates/drizzle/schema.ts +12 -0
- package/templates/env.example +15 -0
- package/templates/fastapi/app/__init__.py +1 -0
- package/templates/fastapi/app/config.py +12 -0
- package/templates/fastapi/app/database.py +16 -0
- package/templates/fastapi/app/models.py +13 -0
- package/templates/fastapi/main.py +22 -0
- package/templates/fastify/index.ts +40 -0
- package/templates/github/CODEOWNERS +10 -0
- package/templates/github/ISSUE_TEMPLATE/bug_report.md +39 -0
- package/templates/github/ISSUE_TEMPLATE/feature_request.md +23 -0
- package/templates/github/PULL_REQUEST_TEMPLATE.md +25 -0
- package/templates/gitignore/node.gitignore +41 -0
- package/templates/gitignore/python.gitignore +49 -0
- package/templates/multi-api/README.md +60 -0
- package/templates/multi-api/gitignore +28 -0
- package/templates/multi-api/node-api/drizzle.config.ts +10 -0
- package/templates/multi-api/node-api/package-simple.json +13 -0
- package/templates/multi-api/node-api/package.json +16 -0
- package/templates/multi-api/node-api/src/db/index.ts +13 -0
- package/templates/multi-api/node-api/src/db/schema.ts +9 -0
- package/templates/multi-api/node-api/src/index-simple.ts +36 -0
- package/templates/multi-api/node-api/src/index.ts +50 -0
- package/templates/multi-api/node-api/tsconfig.json +20 -0
- package/templates/multi-api/python-api/app/__init__.py +1 -0
- package/templates/multi-api/python-api/app/config.py +12 -0
- package/templates/multi-api/python-api/app/database.py +16 -0
- package/templates/multi-api/python-api/app/models.py +13 -0
- package/templates/multi-api/python-api/main-simple.py +25 -0
- package/templates/multi-api/python-api/main.py +44 -0
- package/templates/multi-api/python-api/requirements-simple.txt +3 -0
- package/templates/multi-api/python-api/requirements.txt +8 -0
- package/templates/next/globals.css +126 -0
- package/templates/next/layout.tsx +34 -0
- package/templates/next/next.config.static.ts +10 -0
- package/templates/next/page-fullstack.tsx +120 -0
- package/templates/next/page.tsx +72 -0
- package/templates/presets.json +581 -0
- package/templates/ruff.toml +30 -0
- package/templates/tsconfig.base.json +17 -0
- package/templates/vite/index.css +127 -0
- package/templates/vite/vite.config.ts +7 -0
- package/templates/worker/py/cron.py +53 -0
- package/templates/worker/py/worker.py +95 -0
- package/templates/worker/py/workflow.py +73 -0
- package/templates/worker/ts/cron.ts +49 -0
- package/templates/worker/ts/worker.ts +84 -0
- package/templates/worker/ts/workflow.ts +67 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Vite + React SPA conventions
|
|
3
|
+
globs: ["**/vite.config.*", "**/src/**/*.tsx", "**/src/**/*.ts"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Vite + React SPA Conventions
|
|
7
|
+
|
|
8
|
+
## When to Use Vite vs Next.js
|
|
9
|
+
|
|
10
|
+
**Use Vite when:**
|
|
11
|
+
- Building a pure SPA (no SSR needed)
|
|
12
|
+
- API is separate (backend in different service)
|
|
13
|
+
- Simple static site or dashboard
|
|
14
|
+
- Maximum build speed
|
|
15
|
+
|
|
16
|
+
**Use Next.js when:**
|
|
17
|
+
- Need SSR or static generation
|
|
18
|
+
- SEO is important
|
|
19
|
+
- Full-stack with API routes
|
|
20
|
+
- Complex routing needs
|
|
21
|
+
|
|
22
|
+
## Project Structure
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
src/
|
|
26
|
+
├── main.tsx # Entry point
|
|
27
|
+
├── App.tsx # Root component
|
|
28
|
+
├── index.css # Global styles (Tailwind)
|
|
29
|
+
├── components/ # Reusable components
|
|
30
|
+
│ ├── ui/ # Base UI components
|
|
31
|
+
│ └── features/ # Feature-specific components
|
|
32
|
+
├── hooks/ # Custom hooks
|
|
33
|
+
├── lib/ # Utilities
|
|
34
|
+
│ └── utils.ts # cn() and helpers
|
|
35
|
+
├── types/ # TypeScript types
|
|
36
|
+
└── assets/ # Static assets
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Vite Configuration
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// vite.config.ts
|
|
43
|
+
import { defineConfig } from "vite";
|
|
44
|
+
import react from "@vitejs/plugin-react";
|
|
45
|
+
import path from "path";
|
|
46
|
+
|
|
47
|
+
export default defineConfig({
|
|
48
|
+
plugins: [react()],
|
|
49
|
+
resolve: {
|
|
50
|
+
alias: {
|
|
51
|
+
"@": path.resolve(__dirname, "./src"),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
server: {
|
|
55
|
+
port: 3000,
|
|
56
|
+
proxy: {
|
|
57
|
+
"/api": {
|
|
58
|
+
target: "http://localhost:8080",
|
|
59
|
+
changeOrigin: true,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Path Aliases
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
// tsconfig.json
|
|
70
|
+
{
|
|
71
|
+
"compilerOptions": {
|
|
72
|
+
"baseUrl": ".",
|
|
73
|
+
"paths": {
|
|
74
|
+
"@/*": ["./src/*"]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Usage:
|
|
81
|
+
```typescript
|
|
82
|
+
import { Button } from "@/components/ui/Button";
|
|
83
|
+
import { useAuth } from "@/hooks/useAuth";
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Environment Variables
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# .env
|
|
90
|
+
VITE_API_URL=http://localhost:8080
|
|
91
|
+
VITE_APP_NAME=My App
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// Access in code - must be prefixed with VITE_
|
|
96
|
+
const apiUrl = import.meta.env.VITE_API_URL;
|
|
97
|
+
|
|
98
|
+
// Type definitions
|
|
99
|
+
// src/vite-env.d.ts
|
|
100
|
+
/// <reference types="vite/client" />
|
|
101
|
+
|
|
102
|
+
interface ImportMetaEnv {
|
|
103
|
+
readonly VITE_API_URL: string;
|
|
104
|
+
readonly VITE_APP_NAME: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface ImportMeta {
|
|
108
|
+
readonly env: ImportMetaEnv;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Entry Point
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
// main.tsx
|
|
116
|
+
import React from "react";
|
|
117
|
+
import ReactDOM from "react-dom/client";
|
|
118
|
+
import App from "./App";
|
|
119
|
+
import "./index.css";
|
|
120
|
+
|
|
121
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
122
|
+
<React.StrictMode>
|
|
123
|
+
<App />
|
|
124
|
+
</React.StrictMode>
|
|
125
|
+
);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## API Calls
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// lib/api.ts
|
|
132
|
+
const API_URL = import.meta.env.VITE_API_URL;
|
|
133
|
+
|
|
134
|
+
export async function fetchData<T>(endpoint: string): Promise<T> {
|
|
135
|
+
const response = await fetch(`${API_URL}${endpoint}`);
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
throw new Error(`HTTP ${response.status}`);
|
|
138
|
+
}
|
|
139
|
+
return response.json();
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Scripts
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
// package.json
|
|
147
|
+
{
|
|
148
|
+
"scripts": {
|
|
149
|
+
"dev": "vite",
|
|
150
|
+
"build": "tsc && vite build",
|
|
151
|
+
"preview": "vite preview",
|
|
152
|
+
"lint": "biome check --write src/"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Build Output
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Build for production
|
|
161
|
+
npm run build
|
|
162
|
+
|
|
163
|
+
# Output in dist/
|
|
164
|
+
dist/
|
|
165
|
+
├── index.html
|
|
166
|
+
├── assets/
|
|
167
|
+
│ ├── index-[hash].js
|
|
168
|
+
│ └── index-[hash].css
|
|
169
|
+
```
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Render Workflows conventions for distributed task execution
|
|
3
|
+
globs: ["**/workflow/**/*.ts", "**/workflow/**/*.py", "**/workflows/**/*.ts", "**/workflows/**/*.py", "**/worker/**/*.ts", "**/worker/**/*.py"]
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Render Workflows Conventions
|
|
7
|
+
|
|
8
|
+
Render Workflows provide managed execution of distributed tasks with rapid spin-up and automatic retries. Use workflows for ETL pipelines, AI agents, or any job that benefits from distributed background execution.
|
|
9
|
+
|
|
10
|
+
## SDK Installation
|
|
11
|
+
|
|
12
|
+
TypeScript:
|
|
13
|
+
```bash
|
|
14
|
+
npm install @renderinc/sdk
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Python:
|
|
18
|
+
```bash
|
|
19
|
+
pip install render_sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Project Structure
|
|
23
|
+
|
|
24
|
+
TypeScript:
|
|
25
|
+
```
|
|
26
|
+
workflow/
|
|
27
|
+
├── src/
|
|
28
|
+
│ ├── tasks/
|
|
29
|
+
│ │ ├── index.ts # Task exports
|
|
30
|
+
│ │ ├── math.ts # Domain-specific tasks
|
|
31
|
+
│ │ └── data.ts # Data processing tasks
|
|
32
|
+
│ └── main.ts # Entry point
|
|
33
|
+
├── package.json
|
|
34
|
+
└── tsconfig.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Python:
|
|
38
|
+
```
|
|
39
|
+
workflow/
|
|
40
|
+
├── tasks/
|
|
41
|
+
│ ├── __init__.py
|
|
42
|
+
│ ├── math.py # Domain-specific tasks
|
|
43
|
+
│ └── data.py # Data processing tasks
|
|
44
|
+
├── main.py # Entry point
|
|
45
|
+
└── requirements.txt
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Defining Tasks
|
|
49
|
+
|
|
50
|
+
### TypeScript
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import "dotenv/config";
|
|
54
|
+
import { task, type Retry } from "@renderinc/sdk/workflows";
|
|
55
|
+
|
|
56
|
+
// Retry configuration
|
|
57
|
+
const retry: Retry = {
|
|
58
|
+
maxRetries: 3,
|
|
59
|
+
waitDurationMs: 1000,
|
|
60
|
+
factor: 1.5,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Subtask: assign to const (called by other tasks)
|
|
64
|
+
const square = task({ name: "square" }, function square(a: number): number {
|
|
65
|
+
console.log(`Calculating square of ${a}`);
|
|
66
|
+
return a * a;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Root task: don't assign to variable
|
|
70
|
+
task(
|
|
71
|
+
{
|
|
72
|
+
name: "processData",
|
|
73
|
+
timeoutSeconds: 300,
|
|
74
|
+
retry,
|
|
75
|
+
},
|
|
76
|
+
async function processData(input: string): Promise<string> {
|
|
77
|
+
return input.toUpperCase();
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Root task that calls subtasks
|
|
82
|
+
task(
|
|
83
|
+
{ name: "addSquares", timeoutSeconds: 300, retry },
|
|
84
|
+
async function addSquares(a: number, b: number): Promise<number> {
|
|
85
|
+
const result1 = await square(a);
|
|
86
|
+
const result2 = await square(b);
|
|
87
|
+
return result1 + result2;
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Python
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import asyncio
|
|
96
|
+
import logging
|
|
97
|
+
|
|
98
|
+
from dotenv import load_dotenv
|
|
99
|
+
from render_sdk import Retry, Workflows
|
|
100
|
+
|
|
101
|
+
load_dotenv()
|
|
102
|
+
|
|
103
|
+
logger = logging.getLogger(__name__)
|
|
104
|
+
|
|
105
|
+
# Retry configuration
|
|
106
|
+
retry = Retry(max_retries=3, wait_duration_ms=1000, backoff_scaling=1.5)
|
|
107
|
+
|
|
108
|
+
# Initialize Workflows app
|
|
109
|
+
app = Workflows(
|
|
110
|
+
default_retry=retry,
|
|
111
|
+
default_timeout=300,
|
|
112
|
+
auto_start=True,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Subtask (called by other tasks)
|
|
116
|
+
@app.task
|
|
117
|
+
def square(a: int) -> int:
|
|
118
|
+
"""Square a number."""
|
|
119
|
+
return a * a
|
|
120
|
+
|
|
121
|
+
# Root task that calls subtasks
|
|
122
|
+
@app.task
|
|
123
|
+
async def add_squares(a: int, b: int) -> int:
|
|
124
|
+
"""Add the squares of two numbers."""
|
|
125
|
+
result1 = await square(a)
|
|
126
|
+
result2 = await square(b)
|
|
127
|
+
return result1 + result2
|
|
128
|
+
|
|
129
|
+
@app.task
|
|
130
|
+
async def process_data(data: str) -> str:
|
|
131
|
+
"""Process data."""
|
|
132
|
+
return data.upper()
|
|
133
|
+
|
|
134
|
+
# Fan-out pattern
|
|
135
|
+
@app.task
|
|
136
|
+
async def fan_out(items: list[str]) -> list[str]:
|
|
137
|
+
"""Process items in parallel."""
|
|
138
|
+
tasks = [process_data(item) for item in items]
|
|
139
|
+
results = await asyncio.gather(*tasks)
|
|
140
|
+
return list(results)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Running Tasks (Client)
|
|
144
|
+
|
|
145
|
+
### TypeScript
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { Render, ClientError, ServerError, AbortError } from "@renderinc/sdk";
|
|
149
|
+
|
|
150
|
+
const render = new Render(); // Uses RENDER_API_KEY env var
|
|
151
|
+
|
|
152
|
+
async function runWorkflow() {
|
|
153
|
+
try {
|
|
154
|
+
// Run task and wait for completion
|
|
155
|
+
const result = await render.workflows.runTask(
|
|
156
|
+
"my-workflow/process-data",
|
|
157
|
+
["input-value"]
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
console.log("Status:", result.status);
|
|
161
|
+
console.log("Results:", result.results);
|
|
162
|
+
|
|
163
|
+
// List recent task runs
|
|
164
|
+
const runs = await render.workflows.listTaskRuns({ limit: 10 });
|
|
165
|
+
|
|
166
|
+
// Get specific task run details
|
|
167
|
+
const details = await render.workflows.getTaskRun(result.id);
|
|
168
|
+
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof ClientError) {
|
|
171
|
+
console.error("Client error:", error.statusCode);
|
|
172
|
+
} else if (error instanceof ServerError) {
|
|
173
|
+
console.error("Server error:", error.statusCode);
|
|
174
|
+
} else if (error instanceof AbortError) {
|
|
175
|
+
console.error("Request aborted");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Python
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
import asyncio
|
|
185
|
+
from render_sdk import Render
|
|
186
|
+
from render_sdk.client import ListTaskRunsParams
|
|
187
|
+
from render_sdk.client.errors import RenderError, TaskRunError
|
|
188
|
+
|
|
189
|
+
async def run_workflow():
|
|
190
|
+
render = Render() # Uses RENDER_API_KEY env var
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
# Run task and get awaitable result
|
|
194
|
+
task_run = await render.workflows.run_task(
|
|
195
|
+
"my-workflow/process-data",
|
|
196
|
+
{"arg1": "value"}
|
|
197
|
+
)
|
|
198
|
+
print(f"Task started: {task_run.id}")
|
|
199
|
+
|
|
200
|
+
# Wait for completion
|
|
201
|
+
result = await task_run
|
|
202
|
+
print(f"Status: {result.status}")
|
|
203
|
+
print(f"Results: {result.results}")
|
|
204
|
+
|
|
205
|
+
# List recent task runs
|
|
206
|
+
params = ListTaskRunsParams(limit=10)
|
|
207
|
+
runs = await render.workflows.list_task_runs(params)
|
|
208
|
+
|
|
209
|
+
except TaskRunError as e:
|
|
210
|
+
print(f"Task failed: {e}")
|
|
211
|
+
except RenderError as e:
|
|
212
|
+
print(f"API error: {e}")
|
|
213
|
+
|
|
214
|
+
asyncio.run(run_workflow())
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Best Practices
|
|
218
|
+
|
|
219
|
+
### Task Design
|
|
220
|
+
|
|
221
|
+
1. **Keep tasks focused**: Each task should do one thing well
|
|
222
|
+
2. **Use subtasks for parallelism**: Fan out work across multiple instances
|
|
223
|
+
3. **Make tasks idempotent**: Tasks may retry, so handle duplicate execution
|
|
224
|
+
4. **Return JSON-serializable data**: All inputs and outputs must be JSON-compatible
|
|
225
|
+
|
|
226
|
+
### Retry Configuration
|
|
227
|
+
|
|
228
|
+
Import and type your retry config:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { task, type Retry } from "@renderinc/sdk/workflows";
|
|
232
|
+
|
|
233
|
+
// Retry configuration
|
|
234
|
+
const retry: Retry = {
|
|
235
|
+
maxRetries: 3,
|
|
236
|
+
waitDurationMs: 1000,
|
|
237
|
+
factor: 1.5, // Exponential backoff: 1s, 1.5s, 2.25s
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
task(
|
|
241
|
+
{ name: "fetchData", timeoutSeconds: 300, retry },
|
|
242
|
+
async function fetchData(url: string) {
|
|
243
|
+
const response = await fetch(url);
|
|
244
|
+
return response.json();
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// For non-idempotent operations, disable retries
|
|
249
|
+
task(
|
|
250
|
+
{ name: "sendEmail", retry: { maxRetries: 0, waitDurationMs: 0 } },
|
|
251
|
+
async function sendEmail(to: string, subject: string) {
|
|
252
|
+
// Don't retry to avoid duplicates
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Parallelizing Subtasks
|
|
258
|
+
|
|
259
|
+
TypeScript:
|
|
260
|
+
```typescript
|
|
261
|
+
task(
|
|
262
|
+
{ name: "processImages" },
|
|
263
|
+
async function processImages(urls: string[]): Promise<string[]> {
|
|
264
|
+
// Run all subtasks in parallel
|
|
265
|
+
const results = await Promise.all(
|
|
266
|
+
urls.map(url => processImage(url))
|
|
267
|
+
);
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Python:
|
|
274
|
+
```python
|
|
275
|
+
@app.task
|
|
276
|
+
async def process_images(urls: list[str]) -> list[str]:
|
|
277
|
+
"""Process multiple images in parallel."""
|
|
278
|
+
tasks = [process_image(url) for url in urls]
|
|
279
|
+
results = await asyncio.gather(*tasks)
|
|
280
|
+
return list(results)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Error Handling
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
task(
|
|
287
|
+
{ name: "robustTask" },
|
|
288
|
+
async function robustTask(data: unknown): Promise<Result> {
|
|
289
|
+
try {
|
|
290
|
+
// Main logic
|
|
291
|
+
return await processData(data);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
// Log for observability
|
|
294
|
+
console.error("Task failed:", error);
|
|
295
|
+
|
|
296
|
+
// Re-throw to trigger retry (if configured)
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Cancellation Support (TypeScript)
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
const render = new Render();
|
|
307
|
+
const controller = new AbortController();
|
|
308
|
+
|
|
309
|
+
// Cancel after timeout
|
|
310
|
+
setTimeout(() => controller.abort(), 30000);
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const result = await render.workflows.runTask(
|
|
314
|
+
"my-workflow/long-task",
|
|
315
|
+
[data],
|
|
316
|
+
controller.signal
|
|
317
|
+
);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (error instanceof AbortError) {
|
|
320
|
+
console.log("Task cancelled");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Environment Variables
|
|
326
|
+
|
|
327
|
+
Required:
|
|
328
|
+
- `RENDER_API_KEY` - Your Render API key
|
|
329
|
+
|
|
330
|
+
Optional:
|
|
331
|
+
- `RENDER_USE_LOCAL_DEV` - Enable local development mode (`true`/`false`)
|
|
332
|
+
- `RENDER_LOCAL_DEV_URL` - Local dev URL (default: `http://localhost:8120`)
|
|
333
|
+
|
|
334
|
+
## Task Identifier Format
|
|
335
|
+
|
|
336
|
+
Tasks are identified by: `{workflow-slug}/{task-name}`
|
|
337
|
+
|
|
338
|
+
Example: `my-workflow/process-data`
|
|
339
|
+
|
|
340
|
+
The workflow slug comes from your Render Dashboard. The task name is either:
|
|
341
|
+
- The function name (default)
|
|
342
|
+
- A custom name specified in task options
|
|
343
|
+
|
|
344
|
+
## Limitations
|
|
345
|
+
|
|
346
|
+
- Task instances can run for up to 2 hours
|
|
347
|
+
- Task instances cannot receive incoming network connections
|
|
348
|
+
- All task arguments and return values must be JSON-serializable
|
|
349
|
+
- Workflows currently require early access from Render
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
api:
|
|
5
|
+
build: .
|
|
6
|
+
ports:
|
|
7
|
+
- "3000:3000"
|
|
8
|
+
env_file:
|
|
9
|
+
- .env
|
|
10
|
+
depends_on:
|
|
11
|
+
db:
|
|
12
|
+
condition: service_healthy
|
|
13
|
+
restart: unless-stopped
|
|
14
|
+
|
|
15
|
+
db:
|
|
16
|
+
image: postgres:16-alpine
|
|
17
|
+
ports:
|
|
18
|
+
- "5432:5432"
|
|
19
|
+
environment:
|
|
20
|
+
POSTGRES_USER: postgres
|
|
21
|
+
POSTGRES_PASSWORD: postgres
|
|
22
|
+
POSTGRES_DB: app
|
|
23
|
+
volumes:
|
|
24
|
+
- postgres_data:/var/lib/postgresql/data
|
|
25
|
+
healthcheck:
|
|
26
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
27
|
+
interval: 5s
|
|
28
|
+
timeout: 5s
|
|
29
|
+
retries: 5
|
|
30
|
+
|
|
31
|
+
# Uncomment for Redis
|
|
32
|
+
# redis:
|
|
33
|
+
# image: redis:7-alpine
|
|
34
|
+
# ports:
|
|
35
|
+
# - "6379:6379"
|
|
36
|
+
# volumes:
|
|
37
|
+
# - redis_data:/data
|
|
38
|
+
|
|
39
|
+
# Uncomment for MinIO (S3-compatible storage)
|
|
40
|
+
# minio:
|
|
41
|
+
# image: minio/minio
|
|
42
|
+
# ports:
|
|
43
|
+
# - "9000:9000"
|
|
44
|
+
# - "9001:9001"
|
|
45
|
+
# environment:
|
|
46
|
+
# MINIO_ROOT_USER: minioadmin
|
|
47
|
+
# MINIO_ROOT_PASSWORD: minioadmin
|
|
48
|
+
# volumes:
|
|
49
|
+
# - minio_data:/data
|
|
50
|
+
# command: server /data --console-address ":9001"
|
|
51
|
+
|
|
52
|
+
volumes:
|
|
53
|
+
postgres_data:
|
|
54
|
+
# redis_data:
|
|
55
|
+
# minio_data:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
2
|
+
import postgres from "postgres";
|
|
3
|
+
import * as schema from "./schema";
|
|
4
|
+
|
|
5
|
+
const connectionString = process.env.DATABASE_URL;
|
|
6
|
+
|
|
7
|
+
// Create a placeholder client if DATABASE_URL is not set
|
|
8
|
+
// This allows the app to start without a database for local development
|
|
9
|
+
const client = connectionString
|
|
10
|
+
? postgres(connectionString)
|
|
11
|
+
: postgres("postgres://placeholder:placeholder@localhost:5432/placeholder");
|
|
12
|
+
|
|
13
|
+
export const db = drizzle(client, { schema });
|
|
14
|
+
|
|
15
|
+
export const isDatabaseConfigured = !!connectionString;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
export const users = pgTable("users", {
|
|
4
|
+
id: serial("id").primaryKey(),
|
|
5
|
+
name: text("name").notNull(),
|
|
6
|
+
email: text("email").notNull().unique(),
|
|
7
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
8
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Add more tables here as needed
|
|
12
|
+
// export const posts = pgTable("posts", { ... });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Application
|
|
2
|
+
NODE_ENV=development
|
|
3
|
+
PORT=3000
|
|
4
|
+
|
|
5
|
+
# Database
|
|
6
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
|
7
|
+
|
|
8
|
+
# External APIs
|
|
9
|
+
# API_KEY=your-api-key-here
|
|
10
|
+
|
|
11
|
+
# Storage (S3/MinIO)
|
|
12
|
+
# S3_ENDPOINT=http://localhost:9000
|
|
13
|
+
# S3_ACCESS_KEY=minioadmin
|
|
14
|
+
# S3_SECRET_KEY=minioadmin
|
|
15
|
+
# S3_BUCKET=uploads
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# App package
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from sqlalchemy import create_engine
|
|
2
|
+
from sqlalchemy.orm import sessionmaker, declarative_base
|
|
3
|
+
from app.config import settings
|
|
4
|
+
|
|
5
|
+
engine = create_engine(settings.database_url)
|
|
6
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
7
|
+
|
|
8
|
+
Base = declarative_base()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_db():
|
|
12
|
+
db = SessionLocal()
|
|
13
|
+
try:
|
|
14
|
+
yield db
|
|
15
|
+
finally:
|
|
16
|
+
db.close()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from sqlalchemy import Column, Integer, String, DateTime
|
|
2
|
+
from sqlalchemy.sql import func
|
|
3
|
+
from app.database import Base
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class User(Base):
|
|
7
|
+
__tablename__ = "users"
|
|
8
|
+
|
|
9
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
10
|
+
name = Column(String, nullable=False)
|
|
11
|
+
email = Column(String, unique=True, nullable=False, index=True)
|
|
12
|
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
13
|
+
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from app.config import settings
|
|
3
|
+
from app.database import engine
|
|
4
|
+
from app import models
|
|
5
|
+
|
|
6
|
+
# Create database tables
|
|
7
|
+
models.Base.metadata.create_all(bind=engine)
|
|
8
|
+
|
|
9
|
+
app = FastAPI(
|
|
10
|
+
title=settings.app_name,
|
|
11
|
+
version="0.1.0",
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.get("/health")
|
|
16
|
+
async def health_check():
|
|
17
|
+
return {"status": "ok"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.get("/")
|
|
21
|
+
async def root():
|
|
22
|
+
return {"message": f"Welcome to {settings.app_name}"}
|