@devwithbobby/loops 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/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.config/commitlint.config.ts +11 -0
- package/.config/lefthook.yml +11 -0
- package/.github/workflows/release.yml +52 -0
- package/.github/workflows/test-and-lint.yml +39 -0
- package/README.md +517 -0
- package/biome.json +45 -0
- package/bun.lock +1166 -0
- package/bunfig.toml +7 -0
- package/convex.json +3 -0
- package/example/CLAUDE.md +106 -0
- package/example/README.md +21 -0
- package/example/bun-env.d.ts +17 -0
- package/example/convex/_generated/api.d.ts +53 -0
- package/example/convex/_generated/api.js +23 -0
- package/example/convex/_generated/dataModel.d.ts +60 -0
- package/example/convex/_generated/server.d.ts +149 -0
- package/example/convex/_generated/server.js +90 -0
- package/example/convex/convex.config.ts +7 -0
- package/example/convex/example.ts +76 -0
- package/example/convex/schema.ts +3 -0
- package/example/convex/tsconfig.json +34 -0
- package/example/src/App.tsx +185 -0
- package/example/src/frontend.tsx +39 -0
- package/example/src/index.css +15 -0
- package/example/src/index.html +12 -0
- package/example/src/index.tsx +19 -0
- package/example/tsconfig.json +28 -0
- package/package.json +95 -0
- package/prds/CHANGELOG.md +38 -0
- package/prds/CLAUDE.md +408 -0
- package/prds/CONTRIBUTING.md +274 -0
- package/prds/ENV_SETUP.md +222 -0
- package/prds/MONITORING.md +301 -0
- package/prds/RATE_LIMITING.md +412 -0
- package/prds/SECURITY.md +246 -0
- package/renovate.json +32 -0
- package/src/client/index.ts +530 -0
- package/src/client/types.ts +64 -0
- package/src/component/_generated/api.d.ts +55 -0
- package/src/component/_generated/api.js +23 -0
- package/src/component/_generated/dataModel.d.ts +60 -0
- package/src/component/_generated/server.d.ts +149 -0
- package/src/component/_generated/server.js +90 -0
- package/src/component/convex.config.ts +27 -0
- package/src/component/lib.ts +1125 -0
- package/src/component/schema.ts +17 -0
- package/src/component/tables/contacts.ts +16 -0
- package/src/component/tables/emailOperations.ts +22 -0
- package/src/component/validators.ts +39 -0
- package/src/utils.ts +6 -0
- package/test/client/_generated/_ignore.ts +1 -0
- package/test/client/index.test.ts +65 -0
- package/test/client/setup.test.ts +54 -0
- package/test/component/lib.test.ts +225 -0
- package/test/component/setup.test.ts +21 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changesets
|
|
2
|
+
|
|
3
|
+
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
|
4
|
+
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
|
5
|
+
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
|
6
|
+
|
|
7
|
+
We have a quick list of common questions to get you started engaging with this project in
|
|
8
|
+
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
|
3
|
+
"changelog": [
|
|
4
|
+
"@changesets/changelog-github",
|
|
5
|
+
{ "repo": "samhoque/convex-component-template" }
|
|
6
|
+
],
|
|
7
|
+
"commit": false,
|
|
8
|
+
"fixed": [],
|
|
9
|
+
"linked": [],
|
|
10
|
+
"access": "public",
|
|
11
|
+
"baseBranch": "main",
|
|
12
|
+
"updateInternalDependencies": "patch",
|
|
13
|
+
"ignore": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pre-commit:
|
|
2
|
+
commands:
|
|
3
|
+
check:
|
|
4
|
+
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}"
|
|
5
|
+
run: npx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
|
|
6
|
+
stage_fixed: true
|
|
7
|
+
|
|
8
|
+
commit-msg:
|
|
9
|
+
commands:
|
|
10
|
+
commitlint:
|
|
11
|
+
run: bunx commitlint --config .config/commitlint.config.ts --edit {1}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
# Prevent multiple releases from running simultaneously
|
|
9
|
+
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write # For creating releases and pushing commits
|
|
13
|
+
pull-requests: write # For creating version PRs
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
release:
|
|
17
|
+
name: Release
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
timeout-minutes: 15
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout repository
|
|
23
|
+
uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Setup Bun
|
|
26
|
+
uses: oven-sh/setup-bun@v2
|
|
27
|
+
with:
|
|
28
|
+
bun-version: latest
|
|
29
|
+
|
|
30
|
+
- name: Install dependencies
|
|
31
|
+
run: bun install --frozen-lockfile
|
|
32
|
+
|
|
33
|
+
- name: Build package
|
|
34
|
+
run: bun run build
|
|
35
|
+
|
|
36
|
+
- name: Create Release Pull Request or Publish to npm
|
|
37
|
+
id: changesets
|
|
38
|
+
uses: changesets/action@v1
|
|
39
|
+
with:
|
|
40
|
+
version: bun run ci:version
|
|
41
|
+
publish: bun run ci:publish
|
|
42
|
+
commit: "chore: version packages"
|
|
43
|
+
title: "chore: version packages"
|
|
44
|
+
env:
|
|
45
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
46
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
47
|
+
|
|
48
|
+
- name: Log published packages
|
|
49
|
+
if: steps.changesets.outputs.published == 'true'
|
|
50
|
+
run: |
|
|
51
|
+
echo "Published packages:"
|
|
52
|
+
echo '${{ steps.changesets.outputs.publishedPackages }}' | jq -r '.[] | " - \(.name)@\(.version)"'
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Test and lint
|
|
2
|
+
concurrency:
|
|
3
|
+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
4
|
+
cancel-in-progress: true
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
pull_request:
|
|
10
|
+
branches: ["**"]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
check:
|
|
14
|
+
name: Test and lint
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 30
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Setup Bun
|
|
22
|
+
uses: oven-sh/setup-bun@v2
|
|
23
|
+
with:
|
|
24
|
+
bun-version: latest
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: bun install
|
|
28
|
+
|
|
29
|
+
- name: Build
|
|
30
|
+
run: bun run build
|
|
31
|
+
|
|
32
|
+
- name: Publish package preview
|
|
33
|
+
run: bunx pkg-pr-new publish
|
|
34
|
+
|
|
35
|
+
- name: Test
|
|
36
|
+
run: bun run test
|
|
37
|
+
|
|
38
|
+
- name: Lint
|
|
39
|
+
run: bun run lint
|
package/README.md
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
# 🔨 Convex Component Template
|
|
2
|
+
|
|
3
|
+
[](https://pkg.pr.new/~/robertalv/loops-component)
|
|
4
|
+
|
|
5
|
+
A modern template for building reusable [Convex components](https://www.convex.dev/components) with Bun, TypeScript, and comprehensive testing.
|
|
6
|
+
|
|
7
|
+
> **Note:** Replace `robertalv/loops-component` in the badge above with your GitHub username/organization and repository name once you set up your repository.
|
|
8
|
+
|
|
9
|
+
## Getting Started
|
|
10
|
+
|
|
11
|
+
### 1. Rename Your Component
|
|
12
|
+
|
|
13
|
+
After cloning this template, run the rename script to customize it for your component:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun rename.ts
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The script will:
|
|
20
|
+
- Prompt you for your component name (e.g., "document search", "rate limiter")
|
|
21
|
+
- Ask for your NPM package name (default: `@samhoque/your-component-name`)
|
|
22
|
+
- Ask for your GitHub repository (default: `samhoque/your-component-name`)
|
|
23
|
+
- Automatically generate all case variants (PascalCase, camelCase, kebab-case, etc.)
|
|
24
|
+
- Replace all template placeholders across the entire codebase
|
|
25
|
+
- Update `package.json` with your package name
|
|
26
|
+
- Optionally delete itself when done
|
|
27
|
+
|
|
28
|
+
**What gets renamed:**
|
|
29
|
+
- Package name in `package.json`
|
|
30
|
+
- All imports and references throughout the codebase
|
|
31
|
+
- Component class names, function names, and identifiers
|
|
32
|
+
- Documentation examples in README and comments
|
|
33
|
+
|
|
34
|
+
### 2. Set Up Environment Variables
|
|
35
|
+
|
|
36
|
+
**⚠️ IMPORTANT: Set your Loops API key before using the component.**
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Set the API key in your Convex environment variables
|
|
40
|
+
npx convex env set LOOPS_API_KEY "your-loops-api-key-here"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Or via Convex Dashboard:**
|
|
44
|
+
1. Go to Settings → Environment Variables
|
|
45
|
+
2. Add `LOOPS_API_KEY` with your Loops.so API key
|
|
46
|
+
|
|
47
|
+
📖 **See [ENV_SETUP.md](./ENV_SETUP.md) for detailed setup instructions and security best practices.**
|
|
48
|
+
|
|
49
|
+
### 3. Install Dependencies & Start Development
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bun install
|
|
53
|
+
cd example && bun install && cd ..
|
|
54
|
+
bun run dev:backend
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then in another terminal:
|
|
58
|
+
```bash
|
|
59
|
+
cd example
|
|
60
|
+
bun run dev
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## What are Convex Components?
|
|
64
|
+
|
|
65
|
+
Components are isolated, reusable units of Convex functionality with their own:
|
|
66
|
+
- Functions (queries, mutations, actions)
|
|
67
|
+
- Tables and schemas
|
|
68
|
+
- File storage
|
|
69
|
+
- Scheduled functions
|
|
70
|
+
|
|
71
|
+
Components plug into Convex apps (or parent components) through a public interface, enabling modular architecture with proper isolation and security.
|
|
72
|
+
|
|
73
|
+
## Project Structure
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
src/
|
|
77
|
+
component/ # The Convex component source code
|
|
78
|
+
convex.config.ts # Component configuration (exported via package.json)
|
|
79
|
+
schema.ts # Convex schema definition
|
|
80
|
+
lib.ts # Component functions (queries, mutations)
|
|
81
|
+
_generated/ # Auto-generated Convex types (gitignored)
|
|
82
|
+
|
|
83
|
+
client/ # Optional: Client library that runs in the app
|
|
84
|
+
index.ts # Helper class for easier component interaction
|
|
85
|
+
|
|
86
|
+
react/ # Optional: React components for UI
|
|
87
|
+
index.tsx # React hooks and components
|
|
88
|
+
|
|
89
|
+
test/
|
|
90
|
+
component/ # Component tests (separate from source)
|
|
91
|
+
setup.test.ts # Test setup with module auto-discovery
|
|
92
|
+
*.test.ts # Unit tests for the component
|
|
93
|
+
|
|
94
|
+
example/
|
|
95
|
+
convex/ # Example app that uses the component
|
|
96
|
+
convex.config.ts # Example app configuration
|
|
97
|
+
schema.ts # Example app schema
|
|
98
|
+
_generated/ # Auto-generated types (gitignored)
|
|
99
|
+
src/ # Example app frontend
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Key Commands
|
|
103
|
+
|
|
104
|
+
### Development
|
|
105
|
+
```bash
|
|
106
|
+
bun run dev:backend # Start Convex dev with live component sources
|
|
107
|
+
bun run build # Build the component for distribution
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Testing
|
|
111
|
+
```bash
|
|
112
|
+
bun test # Run all tests
|
|
113
|
+
bun test --watch # Watch mode
|
|
114
|
+
bun test --coverage # Generate coverage reports
|
|
115
|
+
bun test -t "pattern" # Filter tests by name
|
|
116
|
+
CLAUDECODE=1 bun test # AI-friendly quiet output
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Linting and Formatting
|
|
120
|
+
```bash
|
|
121
|
+
bun run lint # Lint with Biome
|
|
122
|
+
bun run lint:fix # Auto-fix linting issues
|
|
123
|
+
bun run format # Format code with Biome
|
|
124
|
+
bun run check # Run both lint and format checks
|
|
125
|
+
bun run check:fix # Auto-fix all issues
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Git Hooks (Lefthook)
|
|
129
|
+
|
|
130
|
+
This project uses [Lefthook](https://github.com/evilmartians/lefthook) for Git hooks. Hooks are automatically installed when you run `bun install`.
|
|
131
|
+
|
|
132
|
+
**Pre-commit hook:**
|
|
133
|
+
- Runs Biome check on staged files
|
|
134
|
+
- Auto-fixes issues and stages the changes
|
|
135
|
+
- Prevents commits with linting/formatting errors
|
|
136
|
+
|
|
137
|
+
To skip hooks (not recommended):
|
|
138
|
+
```bash
|
|
139
|
+
git commit --no-verify
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### CI/CD
|
|
143
|
+
|
|
144
|
+
The project includes a GitHub Actions workflow that runs on every push and pull request:
|
|
145
|
+
|
|
146
|
+
**Workflow: Test and lint** (`.github/workflows/test-and-lint.yml`)
|
|
147
|
+
- Installs dependencies with Bun
|
|
148
|
+
- Builds the project
|
|
149
|
+
- Publishes preview packages with `pkg.pr.new`
|
|
150
|
+
- Runs all tests
|
|
151
|
+
- Runs linting checks
|
|
152
|
+
|
|
153
|
+
The workflow ensures code quality and prevents broken builds from being merged.
|
|
154
|
+
|
|
155
|
+
#### pkg.pr.new Setup
|
|
156
|
+
|
|
157
|
+
This project uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for continuous package previews. Each commit and PR automatically generates a preview release that can be installed without publishing to npm.
|
|
158
|
+
|
|
159
|
+
**One-time setup required:**
|
|
160
|
+
1. Install the [pkg.pr.new GitHub App](https://github.com/apps/pkg-pr-new) on your repository
|
|
161
|
+
2. Once installed, the workflow will automatically publish preview packages on every commit/PR
|
|
162
|
+
|
|
163
|
+
**Using preview packages:**
|
|
164
|
+
```bash
|
|
165
|
+
# Install from a specific commit (Bun)
|
|
166
|
+
bun add https://pkg.pr.new/robertalv/loops-component/robertalv/loops-component@COMMIT_SHA
|
|
167
|
+
|
|
168
|
+
# Or with npm
|
|
169
|
+
npm i https://pkg.pr.new/robertalv/loops-component/robertalv/loops-component@COMMIT_SHA
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Preview URLs will be posted as comments on your pull requests automatically.
|
|
173
|
+
|
|
174
|
+
## Component Architecture
|
|
175
|
+
|
|
176
|
+
### 1. Component Definition
|
|
177
|
+
|
|
178
|
+
The component is defined in `src/component/convex.config.ts`:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { defineComponent } from "convex/server";
|
|
182
|
+
import { api } from "./_generated/api";
|
|
183
|
+
|
|
184
|
+
const component = defineComponent("loopsComponent"); // Change "loopsComponent" to your component name
|
|
185
|
+
component.export(api, { greet: api.lib.greet });
|
|
186
|
+
export default component;
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 2. Package Exports
|
|
190
|
+
|
|
191
|
+
The `package.json` exports the component using the `@convex-dev/component-source` condition:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"exports": {
|
|
196
|
+
"./convex.config": {
|
|
197
|
+
"@convex-dev/component-source": "./src/component/convex.config.ts"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
This enables live reloading during development.
|
|
204
|
+
|
|
205
|
+
### 3. Component Usage
|
|
206
|
+
|
|
207
|
+
Apps import and mount the component in their `convex.config.ts`:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { defineApp } from "convex/server";
|
|
211
|
+
import component from "@your-package/convex.config";
|
|
212
|
+
|
|
213
|
+
const app = defineApp();
|
|
214
|
+
app.use(component);
|
|
215
|
+
export default app;
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 4. Client Library (Optional)
|
|
219
|
+
|
|
220
|
+
The `src/client/` directory contains helper code that runs in the app (not the component):
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
export class Counter {
|
|
224
|
+
constructor(
|
|
225
|
+
private component: UseApi<typeof api>,
|
|
226
|
+
private options?: { initialValue?: number }
|
|
227
|
+
) {}
|
|
228
|
+
|
|
229
|
+
async count(ctx: RunQueryCtx) {
|
|
230
|
+
return await ctx.runQuery(this.component.public.count, {});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
This pattern is useful for:
|
|
236
|
+
- Hiding implementation details
|
|
237
|
+
- Managing implicit dependencies (auth, env vars)
|
|
238
|
+
- Providing a cleaner API surface
|
|
239
|
+
|
|
240
|
+
## Calling Component Functions
|
|
241
|
+
|
|
242
|
+
### Subtransactions
|
|
243
|
+
|
|
244
|
+
Components use **subtransactions** for cross-component function calls:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
// From the app or parent component
|
|
248
|
+
const count = await ctx.runQuery(components.counter.public.count, args);
|
|
249
|
+
const newCount = await ctx.runMutation(components.counter.public.increment, args);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Key semantics:**
|
|
253
|
+
1. Sub-queries track reads for reactivity across components
|
|
254
|
+
2. Sub-mutations contribute to the parent's ACID transaction
|
|
255
|
+
3. Sub-mutations are isolated from each other (even with `Promise.all`)
|
|
256
|
+
4. Parent errors roll back sub-mutations
|
|
257
|
+
5. Sub-mutation errors can be caught without affecting parent
|
|
258
|
+
|
|
259
|
+
### Exposing Component Functions Publicly
|
|
260
|
+
|
|
261
|
+
Components cannot be called directly from clients. The app must wrap them:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// in the app's convex/counter.ts
|
|
265
|
+
export const count = query({
|
|
266
|
+
handler: async (ctx) => {
|
|
267
|
+
return await ctx.runQuery(components.counter.public.count, {});
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
This allows the app to add auth, rate limiting, etc.
|
|
273
|
+
|
|
274
|
+
## Working with Isolation
|
|
275
|
+
|
|
276
|
+
### Function Access Hierarchy
|
|
277
|
+
|
|
278
|
+
```mermaid
|
|
279
|
+
flowchart TD
|
|
280
|
+
A[Public Internet / React] --> B[App]
|
|
281
|
+
B --> C[Component1]
|
|
282
|
+
B --> D[Component2]
|
|
283
|
+
C --> E[Component3]
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- Clients can only call app functions (not component functions)
|
|
287
|
+
- Apps can call their own functions and component public functions
|
|
288
|
+
- Components can only call their own functions and child component public functions
|
|
289
|
+
|
|
290
|
+
### Function Handles
|
|
291
|
+
|
|
292
|
+
To allow components to call back into the app, use function handles:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// In the app
|
|
296
|
+
const handle = await createFunctionHandle(api.myMutation);
|
|
297
|
+
|
|
298
|
+
// Pass handle to component
|
|
299
|
+
await ctx.runMutation(components.worker.public.process, { handler: handle });
|
|
300
|
+
|
|
301
|
+
// In the component
|
|
302
|
+
export const process = mutation({
|
|
303
|
+
args: { handler: v.string() },
|
|
304
|
+
handler: async (ctx, args) => {
|
|
305
|
+
// Component can now call the app's function
|
|
306
|
+
const functionHandle: FunctionHandle<"mutation"> = args.handler;
|
|
307
|
+
await ctx.runMutation(functionHandle, {});
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Use cases:**
|
|
313
|
+
- Migrations component iterating over app tables
|
|
314
|
+
- Webhook handlers calling app logic
|
|
315
|
+
- Background job processors
|
|
316
|
+
|
|
317
|
+
### Table Access
|
|
318
|
+
|
|
319
|
+
Components have **isolated tables**:
|
|
320
|
+
- Components can only read/write their own tables
|
|
321
|
+
- Use `v.id("tableName")` for component tables
|
|
322
|
+
- Use `v.string()` for IDs from other components/app
|
|
323
|
+
- Use function handles to grant table access across boundaries
|
|
324
|
+
|
|
325
|
+
### Environment Variables and Auth
|
|
326
|
+
|
|
327
|
+
Components **cannot access** `process.env` or `ctx.auth` directly. Pass them through:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// src/client/index.ts (runs in app context)
|
|
331
|
+
class MyComponent {
|
|
332
|
+
constructor(
|
|
333
|
+
private component: UseApi<typeof api>,
|
|
334
|
+
private options?: { apiKey?: string }
|
|
335
|
+
) {
|
|
336
|
+
this.apiKey = options?.apiKey ?? process.env.MY_API_KEY;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async doSomething(ctx: QueryCtx) {
|
|
340
|
+
return await ctx.runQuery(this.component.public.process, {
|
|
341
|
+
apiKey: this.apiKey,
|
|
342
|
+
auth: await ctx.auth.getUserIdentity(),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### HTTP Actions
|
|
349
|
+
|
|
350
|
+
Components cannot define HTTP routes directly. Instead, they export handlers that the app mounts:
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// src/client/index.ts
|
|
354
|
+
export const httpHandler = httpAction(async (ctx, request) => {
|
|
355
|
+
// Handle HTTP request
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// In app's convex/http.ts
|
|
359
|
+
import { httpRouter } from "convex/server";
|
|
360
|
+
import { httpHandler } from "@your-component/client";
|
|
361
|
+
|
|
362
|
+
const http = httpRouter();
|
|
363
|
+
http.route({ path: "/webhook", method: "POST", handler: httpHandler });
|
|
364
|
+
export default http;
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Pagination
|
|
368
|
+
|
|
369
|
+
The built-in `.paginate()` doesn't work in components. Use [`convex-helpers` paginator](https://github.com/get-convex/convex-helpers) instead:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import { paginationOptsValidator } from "convex-helpers/server/pagination";
|
|
373
|
+
import { makePagination } from "convex-helpers/server/pagination";
|
|
374
|
+
|
|
375
|
+
export const listItems = query({
|
|
376
|
+
args: { paginationOpts: paginationOptsValidator },
|
|
377
|
+
handler: async (ctx, args) => {
|
|
378
|
+
return await makePagination(ctx.db.query("items"), args.paginationOpts);
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Testing Pattern
|
|
384
|
+
|
|
385
|
+
This template uses `convex-test` with Bun's test runner. Tests are **separate from component source** to prevent bundling issues.
|
|
386
|
+
|
|
387
|
+
### Test Setup (`test/component/setup.test.ts`)
|
|
388
|
+
|
|
389
|
+
Auto-discovers component files and creates a test helper:
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
import { convexTest as baseConvexTest } from "convex-test";
|
|
393
|
+
import { Glob } from "bun";
|
|
394
|
+
|
|
395
|
+
const glob = new Glob("**/*.ts");
|
|
396
|
+
const modules: Record<string, string> = {};
|
|
397
|
+
|
|
398
|
+
for await (const file of glob.scan("./src/component")) {
|
|
399
|
+
if (!file.startsWith("_generated/")) {
|
|
400
|
+
modules[file.replace(/\.ts$/, ".js")] = await Bun.file(
|
|
401
|
+
`./src/component/${file}`
|
|
402
|
+
).text();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export const convexTest = () => baseConvexTest(schema, modules);
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Writing Tests
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { test, expect } from "bun:test";
|
|
413
|
+
import { api } from "../../src/component/_generated/api";
|
|
414
|
+
import { convexTest } from "./setup.test";
|
|
415
|
+
|
|
416
|
+
test("greet returns greeting", async () => {
|
|
417
|
+
const t = convexTest();
|
|
418
|
+
const result = await t.query(api.lib.greet, { name: "Alice" });
|
|
419
|
+
expect(result).toBe("Hello, Alice!");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test("with authentication", async () => {
|
|
423
|
+
const t = convexTest();
|
|
424
|
+
const asUser = t.withIdentity({ subject: "user123" });
|
|
425
|
+
const result = await asUser.query(api.lib.getCurrentUser, {});
|
|
426
|
+
expect(result.subject).toBe("user123");
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Distribution
|
|
431
|
+
|
|
432
|
+
### Local Development
|
|
433
|
+
|
|
434
|
+
To use the component in another app during development:
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
bun run build
|
|
438
|
+
bun pack # or npm pack
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Then in the other app:
|
|
442
|
+
```bash
|
|
443
|
+
bun install ../path/to/component/your-component-0.1.0.tgz
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### Publishing
|
|
447
|
+
|
|
448
|
+
This package is currently marked as `private` in package.json. To publish to npm:
|
|
449
|
+
|
|
450
|
+
1. Remove `"private": true` from package.json
|
|
451
|
+
2. Run: `bun publish` (or `npm publish` if bun publish is not yet available)
|
|
452
|
+
|
|
453
|
+
## Dashboard and Deployment
|
|
454
|
+
|
|
455
|
+
### Component Visibility
|
|
456
|
+
|
|
457
|
+
In the Convex dashboard, you can select each component to see:
|
|
458
|
+
- Data tables
|
|
459
|
+
- Functions
|
|
460
|
+
- File storage
|
|
461
|
+
- Logs
|
|
462
|
+
- Scheduled functions
|
|
463
|
+
|
|
464
|
+
### Deployment Semantics
|
|
465
|
+
|
|
466
|
+
1. **Function calls**: Top-level query/mutation counts as single function call (sub-calls are free)
|
|
467
|
+
2. **Database bandwidth**: Component functions count bandwidth separately
|
|
468
|
+
3. **Logging**: Component logs appear in dashboard and log streams
|
|
469
|
+
4. **Exports**: Snapshot exports include all component data
|
|
470
|
+
5. **Streaming exports**: Only include top-level app data (not components)
|
|
471
|
+
|
|
472
|
+
## Code Style
|
|
473
|
+
|
|
474
|
+
- **Package manager**: Bun
|
|
475
|
+
- **Linter/Formatter**: Biome
|
|
476
|
+
- **Indentation**: Tabs
|
|
477
|
+
- **Quotes**: Double quotes
|
|
478
|
+
- **TypeScript**: Strict mode with extra checks
|
|
479
|
+
|
|
480
|
+
## Examples
|
|
481
|
+
|
|
482
|
+
### First-Party Components
|
|
483
|
+
|
|
484
|
+
All first-party components are open source:
|
|
485
|
+
|
|
486
|
+
- [loops-component](https://github.com/get-convex/loops-component) - Attaching components to app tables with triggers
|
|
487
|
+
- [twilio](https://github.com/get-convex/twilio) - HTTP actions and webhooks
|
|
488
|
+
- [aggregate](https://github.com/get-convex/aggregate) - Testing patterns
|
|
489
|
+
- [migrations](https://github.com/get-convex/migrations) - Function handles and table access
|
|
490
|
+
|
|
491
|
+
## Important Notes
|
|
492
|
+
|
|
493
|
+
- **Generated files**: Never edit `_generated/` directories
|
|
494
|
+
- **Test location**: Always place tests in `test/component/` (not `src/component/`)
|
|
495
|
+
- **Component name**: Change `"loopsComponent"` in convex.config.ts to your component name
|
|
496
|
+
- **Live reloading**: Enabled via `--live-component-sources` flag
|
|
497
|
+
- **Peer dependencies**: Component uses the app's Convex installation
|
|
498
|
+
|
|
499
|
+
## Documentation
|
|
500
|
+
|
|
501
|
+
- **[ENV_SETUP.md](./ENV_SETUP.md)** - Environment variable setup and security best practices
|
|
502
|
+
- **[SECURITY.md](./SECURITY.md)** - Security considerations and guidelines
|
|
503
|
+
- **[MONITORING.md](./MONITORING.md)** - Email monitoring and spam detection
|
|
504
|
+
- **[RATE_LIMITING.md](./RATE_LIMITING.md)** - Rate limiting implementation guide
|
|
505
|
+
|
|
506
|
+
## Resources
|
|
507
|
+
|
|
508
|
+
- [Convex Components Documentation](https://www.convex.dev/components)
|
|
509
|
+
- [Component Authoring Guide](https://docs.convex.dev/components/authoring)
|
|
510
|
+
- [Convex Environment Variables](https://docs.convex.dev/production/environment-variables)
|
|
511
|
+
- [convex-test](https://github.com/get-convex/convex-test)
|
|
512
|
+
- [convex-helpers](https://github.com/get-convex/convex-helpers)
|
|
513
|
+
- [Bun Documentation](https://bun.sh/docs)
|
|
514
|
+
|
|
515
|
+
## License
|
|
516
|
+
|
|
517
|
+
MIT
|
package/biome.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": false,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": false
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false,
|
|
10
|
+
"includes": ["**", "!**/_generated", "!dist"]
|
|
11
|
+
},
|
|
12
|
+
"formatter": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"indentStyle": "tab"
|
|
15
|
+
},
|
|
16
|
+
"linter": {
|
|
17
|
+
"enabled": true,
|
|
18
|
+
"rules": {
|
|
19
|
+
"recommended": true
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"javascript": {
|
|
23
|
+
"formatter": {
|
|
24
|
+
"quoteStyle": "double"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"assist": {
|
|
28
|
+
"enabled": true,
|
|
29
|
+
"actions": {
|
|
30
|
+
"source": {
|
|
31
|
+
"organizeImports": "on"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"json": {
|
|
36
|
+
"parser": {
|
|
37
|
+
"allowComments": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"css": {
|
|
41
|
+
"parser": {
|
|
42
|
+
"tailwindDirectives": true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|