mirror of
https://github.com/immich-app/services.git
synced 2026-09-30 13:23:10 +08:00
feat: initial services infrastructure & hello world api (#1)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
name: Build
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
|
||||
TG_NON_INTERACTIVE: 'true'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- name: Run pnpm install
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Upload build output
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: build-output
|
||||
path: 'dist'
|
||||
retention-days: 7
|
||||
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
env:
|
||||
TF_VAR_stage: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || '' }}
|
||||
TF_VAR_dist_dir: ${{github.workspace}}/dist
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ github.ref == 'refs/heads/main' && secrets.OP_TF_PROD_ENV || secrets.OP_TF_DEV_ENV }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Get build artifact
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||
with:
|
||||
name: 'build-output'
|
||||
path: '${{ github.workspace }}/dist'
|
||||
|
||||
- name: Install 1Password CLI
|
||||
uses: 1password/install-cli-action@707af61a6c8b4fa7f9476328c035208fba6b54d7 # v2
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@697a75e2c3186d3c037c2c159855cf2d566542ba # use-mise-action-0.0.1
|
||||
|
||||
- name: Deploy All
|
||||
working-directory: ${{ github.workspace }}/deployment
|
||||
run: mise run tf:apply
|
||||
# - name: Deploy Output
|
||||
# id: deploy-output
|
||||
# working-directory: ${{ github.workspace }}/deployment/modules/cloudflare/static-pages
|
||||
# run: |
|
||||
# echo "output=$(op run --no-masking --env-file='../../../.env' -- terragrunt output -json | jq -c .)" >> $GITHUB_OUTPUT
|
||||
#
|
||||
# - name: Comment
|
||||
# uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3
|
||||
# if: ${{ github.event_name == 'pull_request' }}
|
||||
# with:
|
||||
# number: ${{ github.event.number }}
|
||||
# body: |
|
||||
# 📖 Preview of ${{ matrix.app_name }}.immich.app deployed to [${{ fromJson(steps.deploy-output.outputs.output).immich_subdomain.value }}](https://${{ fromJson(steps.deploy-output.outputs.output).immich_subdomain.value }})
|
||||
# emojis: 'rocket'
|
||||
# body-include: '<!-- ${{ matrix.app_name }} PR URL -->'
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Docs destroy
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
TF_VAR_dist_dir: ${{ github.workspace }}/dist
|
||||
TF_VAR_stage: pr-${{ github.event.number }}
|
||||
OP_SERVICE_ACCOUNT_TOKEN: ${{ matrix.environment == 'prod' && secrets.OP_TF_PROD_ENV || secrets.OP_TF_DEV_ENV }}
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
environment: ['dev', 'prod']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Install 1Password CLI
|
||||
uses: 1password/install-cli-action@707af61a6c8b4fa7f9476328c035208fba6b54d7 # v2
|
||||
|
||||
- name: Setup Mise
|
||||
uses: immich-app/devtools/actions/use-mise@697a75e2c3186d3c037c2c159855cf2d566542ba # use-mise-action-0.0.1
|
||||
|
||||
- name: Destroy All
|
||||
working-directory: ${{ github.workspace }}/deployment
|
||||
run: mise run tf:destroy
|
||||
# - name: Comment
|
||||
# uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3
|
||||
# with:
|
||||
# number: ${{ github.event.number }}
|
||||
# delete: true
|
||||
# body-include: '<!-- Docs PR URL -->'
|
||||
@@ -0,0 +1,43 @@
|
||||
name: Test
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Run pnpm install
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Run linter
|
||||
run: pnpm run lint
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run formatter
|
||||
run: pnpm run format
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run tsc
|
||||
run: pnpm run check
|
||||
if: ${{ !cancelled() }}
|
||||
|
||||
- name: Run unit tests
|
||||
run: pnpm run test
|
||||
if: ${{ !cancelled() }}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
coverage/
|
||||
|
||||
# Production
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Wrangler
|
||||
.wrangler/
|
||||
.dev.vars
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Terraform
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
.terraform/
|
||||
.terragrunt-cache/
|
||||
*.tfplan
|
||||
*.tfvars
|
||||
!*.tfvars.example
|
||||
|
||||
# Misc
|
||||
.cache/
|
||||
tmp/
|
||||
@@ -0,0 +1,44 @@
|
||||
[tools]
|
||||
opentofu = "1.10.3"
|
||||
terragrunt = "0.87.0"
|
||||
|
||||
[env]
|
||||
TF_VAR_dist_dir="{{config_root}}/dist"
|
||||
|
||||
[tasks.tg]
|
||||
run = "op run '--env-file={{config_root}}/deployment/.env' -- terragrunt"
|
||||
description = "Wrapper for terragrunt"
|
||||
dir = "{{cwd}}"
|
||||
|
||||
[tasks."tg:fmt"]
|
||||
run = "mise run tg hclfmt"
|
||||
description = "Format terragrunt files"
|
||||
|
||||
[tasks.tf]
|
||||
run = "mise run tg run --all"
|
||||
description = "Wrapper for terragrunt run-all"
|
||||
dir = "{{cwd}}"
|
||||
|
||||
[tasks."tf:fmt"]
|
||||
run = "tofu fmt -recursive tf/"
|
||||
description = "Format terraform files"
|
||||
|
||||
[tasks."tf:init"]
|
||||
run = "mise run tf init -- -reconfigure"
|
||||
dir = "{{cwd}}"
|
||||
|
||||
[tasks."tf:plan"]
|
||||
run = "mise run tf plan"
|
||||
dir = "{{cwd}}"
|
||||
|
||||
[tasks."tf:apply"]
|
||||
run = "mise run tf apply"
|
||||
dir = "{{cwd}}"
|
||||
|
||||
[tasks."tf:destroy"]
|
||||
run = "mise run tf destroy"
|
||||
dir = "{{cwd}}"
|
||||
|
||||
[settings]
|
||||
experimental = true
|
||||
idiomatic_version_file_enable_tools = []
|
||||
@@ -0,0 +1,10 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
.wrangler/
|
||||
.terragrunt-cache/
|
||||
.terraform/
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
coverage/
|
||||
deployment/
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120,
|
||||
"semi": true,
|
||||
"organizeImportsSkipDestructiveCodeActions": true,
|
||||
"plugins": ["prettier-plugin-organize-imports"]
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is the Immich Workers repository - a monorepo for Cloudflare Workers that provide backend/API services. These workers are deployed independently from the frontend applications.
|
||||
|
||||
## Common Commands
|
||||
|
||||
### Root Level
|
||||
|
||||
```bash
|
||||
pnpm install # Install all dependencies
|
||||
pnpm run lint # Lint all workers
|
||||
pnpm run format # Check formatting
|
||||
pnpm run test # Run all tests
|
||||
pnpm run typecheck # Type-check all workers
|
||||
```
|
||||
|
||||
### Worker Development
|
||||
|
||||
```bash
|
||||
cd apps/<worker-name>
|
||||
pnpm install # Install worker dependencies
|
||||
pnpm run dev # Start development server
|
||||
pnpm run build # Build for production
|
||||
pnpm run deploy # Deploy directly to Cloudflare
|
||||
pnpm run test # Run worker tests
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Repository Structure
|
||||
|
||||
```
|
||||
apps/
|
||||
├── hello/ # Example hello world worker
|
||||
├── datasets/ # Datasets API worker
|
||||
└── .../ # Other worker applications
|
||||
|
||||
deployment/
|
||||
├── modules/ # Terraform modules
|
||||
│ └── cloudflare/
|
||||
│ └── workers/
|
||||
│ └── generic/ # Reusable worker module
|
||||
└── terragrunt/ # Terragrunt configurations
|
||||
├── dev/ # Development environment
|
||||
├── staging/ # Staging environment
|
||||
└── prod/ # Production environment
|
||||
|
||||
src/
|
||||
└── lib/ # Shared libraries and utilities
|
||||
```
|
||||
|
||||
### Worker Structure
|
||||
|
||||
Each worker in `apps/<worker-name>/` contains:
|
||||
|
||||
- `src/index.ts` - Main worker entry point
|
||||
- `wrangler.toml` - Cloudflare Worker configuration
|
||||
- `package.json` - Dependencies and scripts
|
||||
- `tsconfig.json` - TypeScript configuration
|
||||
- `vitest.config.ts` - Test configuration
|
||||
- `worker-configuration.d.ts` - Environment type definitions
|
||||
|
||||
### Technology Stack
|
||||
|
||||
- **Runtime**: Cloudflare Workers
|
||||
- **Language**: TypeScript
|
||||
- **Build Tool**: Wrangler CLI
|
||||
- **Testing**: Vitest with Miniflare
|
||||
- **Infrastructure**: Terraform/Terragrunt
|
||||
- **Package Manager**: pnpm with workspaces
|
||||
|
||||
## Deployment
|
||||
|
||||
### Direct Deployment (Wrangler)
|
||||
|
||||
```bash
|
||||
cd apps/<worker-name>
|
||||
pnpm run deploy # Deploy to default environment
|
||||
pnpm run deploy:staging # Deploy to staging
|
||||
pnpm run deploy:production # Deploy to production
|
||||
```
|
||||
|
||||
### Infrastructure as Code (Terraform/Terragrunt)
|
||||
|
||||
```bash
|
||||
# Set up required environment variables
|
||||
export TF_VAR_tf_state_postgres_conn_str="postgresql://user:pass@host/dbname"
|
||||
export TF_VAR_env="dev" # Environment (dev/staging/prod)
|
||||
export TF_VAR_stage="dev" # Stage
|
||||
export TF_VAR_app_name="hello" # App name
|
||||
export TF_VAR_cloudflare_account_id="your-account-id"
|
||||
|
||||
# Deploy with Terragrunt
|
||||
cd deployment/modules/cloudflare/workers/<worker-name>
|
||||
terragrunt init
|
||||
terragrunt plan
|
||||
terragrunt apply
|
||||
```
|
||||
|
||||
The deployment uses the same Terragrunt pattern as other Immich infrastructure:
|
||||
|
||||
- PostgreSQL backend for state storage
|
||||
- Remote state references for API keys and account info
|
||||
- Schema naming: `cloudflare_workers_immich_app_${app_name}_${env}${stage}`
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### Local Development
|
||||
|
||||
Create `.dev.vars` in the worker directory:
|
||||
|
||||
```
|
||||
SECRET_KEY=local_secret
|
||||
API_ENDPOINT=https://api.example.com
|
||||
```
|
||||
|
||||
### Production Secrets
|
||||
|
||||
Use Wrangler or Terraform to set production secrets:
|
||||
|
||||
```bash
|
||||
wrangler secret put SECRET_KEY
|
||||
```
|
||||
|
||||
## Creating a New Worker
|
||||
|
||||
1. Create worker directory: `apps/<worker-name>/`
|
||||
2. Copy structure from `apps/hello/` as template
|
||||
3. Update `wrangler.toml` with worker name
|
||||
4. Implement worker logic in `src/index.ts`
|
||||
5. Add Terragrunt config if using IaC deployment
|
||||
|
||||
## Testing
|
||||
|
||||
Workers use Vitest with Miniflare for testing:
|
||||
|
||||
```bash
|
||||
cd apps/<worker-name>
|
||||
pnpm run test # Run tests once
|
||||
pnpm run test:watch # Run tests in watch mode
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Request Handling
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
// Handle request
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### CORS Headers
|
||||
|
||||
```typescript
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
};
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
// Worker logic
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ error: 'Internal Server Error' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,83 @@
|
||||
# Immich Workers
|
||||
|
||||
Monorepo for Immich Cloudflare Workers (backend-only/API services).
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
apps/
|
||||
├── hello/ # Hello world example worker
|
||||
├── datasets/ # Datasets API worker
|
||||
└── .../ # Other worker applications
|
||||
|
||||
deployment/
|
||||
├── modules/ # Terraform modules
|
||||
└── terragrunt/ # Terragrunt configurations
|
||||
|
||||
src/
|
||||
└── lib/ # Shared libraries and utilities
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Working with Individual Workers
|
||||
|
||||
```bash
|
||||
cd apps/<worker-name>
|
||||
npm install
|
||||
npm run dev # Start development server
|
||||
npm run test # Run tests
|
||||
npm run build # Build for production
|
||||
npm run deploy # Deploy to Cloudflare
|
||||
```
|
||||
|
||||
### Global Commands
|
||||
|
||||
```bash
|
||||
npm run lint # Lint all workers
|
||||
npm run format # Check formatting
|
||||
npm run test # Run all tests
|
||||
npm run typecheck # Type-check all workers
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Using Wrangler (Direct)
|
||||
|
||||
```bash
|
||||
cd apps/<worker-name>
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
### Using Terraform/Terragrunt
|
||||
|
||||
```bash
|
||||
cd deployment/terragrunt/<environment>/<worker-name>
|
||||
terragrunt plan
|
||||
terragrunt apply
|
||||
```
|
||||
|
||||
## Creating a New Worker
|
||||
|
||||
1. Create directory: `apps/<worker-name>/`
|
||||
2. Add `package.json`, `wrangler.toml`, `tsconfig.json`
|
||||
3. Create source in `src/index.ts`
|
||||
4. Add terraform module in `deployment/modules/cloudflare/workers/<worker-name>/`
|
||||
5. Add terragrunt config in `deployment/terragrunt/`
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Workers use `.dev.vars` for local development and Cloudflare secrets for production.
|
||||
|
||||
Example `.dev.vars`:
|
||||
|
||||
```
|
||||
MY_SECRET=secret_value
|
||||
API_KEY=test_key
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.wrangler/
|
||||
.dev.vars
|
||||
.env
|
||||
.env.local
|
||||
@@ -0,0 +1,25 @@
|
||||
# hello.immich.app
|
||||
|
||||
A simple Cloudflare Worker API example for the Immich project.
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `GET /` - Returns a hello message
|
||||
- `GET /health` - Health check endpoint
|
||||
- `GET /api/greet?name=<name>` - Personalized greeting
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Run locally
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "@immich-services/hello",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wrangler dev",
|
||||
"build": "wrangler deploy --dry-run --outdir ../../dist/hello",
|
||||
"tail": "wrangler tail",
|
||||
"test": "vitest",
|
||||
"check": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { SELF } from 'cloudflare:test';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('Hello Worker', () => {
|
||||
it('should return hello message on root path', async () => {
|
||||
const response = await SELF.fetch('https://example.com/');
|
||||
const data = (await response.json()) as any;
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data).toHaveProperty('message', 'Hello from Immich Worker API!');
|
||||
expect(data).toHaveProperty('timestamp');
|
||||
});
|
||||
|
||||
it('should return health status', async () => {
|
||||
const response = await SELF.fetch('https://example.com/health');
|
||||
const data = (await response.json()) as any;
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data).toHaveProperty('status', 'healthy');
|
||||
});
|
||||
|
||||
it('should greet with name parameter', async () => {
|
||||
const response = await SELF.fetch('https://example.com/api/greet?name=Claude');
|
||||
const data = (await response.json()) as any;
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data).toHaveProperty('greeting', 'Hello, Claude!');
|
||||
});
|
||||
|
||||
it('should return 404 for unknown routes', async () => {
|
||||
const response = await SELF.fetch('https://example.com/unknown');
|
||||
const data = (await response.json()) as any;
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data).toHaveProperty('error', 'Not Found');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
export default {
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Handle different routes
|
||||
switch (url.pathname) {
|
||||
case '/': {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: 'Hello from Immich Worker API!',
|
||||
timestamp: new Date().toISOString(),
|
||||
path: url.pathname,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
case '/health': {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
case '/api/greet': {
|
||||
const name = url.searchParams.get('name') || 'World';
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
greeting: `Hello, ${name}!`,
|
||||
timestamp: new Date().toISOString(),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
default: {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: 'Not Found',
|
||||
path: url.pathname,
|
||||
}),
|
||||
{
|
||||
status: 404,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import baseConfig from '../../vitest.base.config.js';
|
||||
|
||||
export default baseConfig;
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
interface Env {
|
||||
// Add environment variables here as needed
|
||||
// Example:
|
||||
// MY_KV_NAMESPACE: KVNamespace;
|
||||
// MY_DURABLE_OBJECT: DurableObjectNamespace;
|
||||
MY_SECRET: string;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
name = "hello-immich-app"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-01-01"
|
||||
|
||||
# Uncomment to add KV namespaces, Durable Objects, etc.
|
||||
# [[kv_namespaces]]
|
||||
# binding = "MY_KV_NAMESPACE"
|
||||
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
# [[durable_objects.bindings]]
|
||||
# name = "MY_DURABLE_OBJECT"
|
||||
# class_name = "MyDurableObject"
|
||||
# script_name = "my-durable-object"
|
||||
@@ -0,0 +1,4 @@
|
||||
export TF_VAR_cloudflare_account_id="op://tf/cloudflare/account_id"
|
||||
export TF_VAR_cloudflare_api_token="op://tf/cloudflare/api_token"
|
||||
export TF_VAR_tf_state_postgres_conn_str="op://tf/tf_state/postgres_conn_str"
|
||||
export TF_VAR_env=$ENVIRONMENT
|
||||
@@ -0,0 +1,19 @@
|
||||
# This file is maintained automatically by "tofu init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "5.9.0"
|
||||
constraints = "~> 5.0"
|
||||
hashes = [
|
||||
"h1:yNvFe2InCb4foqjWd+Sz9DIX6mdk30iRycsmm/fZarY=",
|
||||
"zh:3ae6f70f4e2961e84b89b337d268db0537c6dd0e66d4ccf32cbacc950f7a2807",
|
||||
"zh:4472d1d1629c3c3a6a23672691ed0852bea9fcd9e39a213d388616f93adaaeb0",
|
||||
"zh:4680cb586233b4702abb9fc69615bca6ebe9547ddaceaa0d0439bccc7c773905",
|
||||
"zh:51fd157b544644438ab827e288c8173ecbf31cd92b3b75a8446569db35c00cab",
|
||||
"zh:66aaeb1cb991b982f81564ea52a18ba962b43e86d9e5c76ac591fa522a4a0db6",
|
||||
"zh:d271308040efe8324633810d8c58142310da5f88eb223422fd13daa73e944316",
|
||||
"zh:e56c66588135878081ffa8c2aa5da969d56ff96d4ecb4b0ab6b8b496830cf7c2",
|
||||
"zh:f809ab383cca0a5f83072981c64208cbd7fa67e986a86ee02dd2c82333221e32",
|
||||
"zh:fde7a7d2bda2784e78c0bcc39155e0b09c31dd4a4fa65c26e0e8fe858445ecfc",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
terraform {
|
||||
backend "pg" {}
|
||||
required_version = "~> 1.7"
|
||||
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "~> 5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
locals {
|
||||
resource_stage = var.stage != "" ? "-${var.stage}" : ""
|
||||
resource_env = "-${var.env}"
|
||||
resource_suffix = "${local.resource_env}${local.resource_stage}"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
provider "cloudflare" {
|
||||
api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_account
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
data "terraform_remote_state" "api_keys_state" {
|
||||
backend = "pg"
|
||||
|
||||
config = {
|
||||
conn_str = var.tf_state_postgres_conn_str
|
||||
schema_name = "prod_cloudflare_api_keys"
|
||||
}
|
||||
}
|
||||
|
||||
data "terraform_remote_state" "cloudflare_account" {
|
||||
backend = "pg"
|
||||
|
||||
config = {
|
||||
conn_str = var.tf_state_postgres_conn_str
|
||||
schema_name = "prod_cloudflare_account"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
terraform {
|
||||
source = "."
|
||||
|
||||
extra_arguments custom_vars {
|
||||
commands = get_terraform_commands_that_need_vars()
|
||||
}
|
||||
}
|
||||
|
||||
include {
|
||||
path = find_in_parent_folders("state.hcl")
|
||||
}
|
||||
|
||||
locals {
|
||||
env = get_env("TF_VAR_env")
|
||||
stage = get_env("TF_VAR_stage")
|
||||
app_name = "hello"
|
||||
}
|
||||
|
||||
inputs = {
|
||||
app_name = local.app_name
|
||||
}
|
||||
|
||||
remote_state {
|
||||
backend = "pg"
|
||||
|
||||
config = {
|
||||
conn_str = get_env("TF_VAR_tf_state_postgres_conn_str")
|
||||
schema_name = "services_cloudflare_workers_${local.app_name}_immich_app_${local.env}${local.stage}"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
variable "tf_state_postgres_conn_str" {}
|
||||
variable "stage" {}
|
||||
variable "env" {}
|
||||
variable "app_name" {}
|
||||
variable "cloudflare_account_id" {}
|
||||
variable "dist_dir" {}
|
||||
@@ -0,0 +1,63 @@
|
||||
resource "cloudflare_worker" "worker" {
|
||||
account_id = var.cloudflare_account_id
|
||||
name = "${var.app_name}-api${local.resource_suffix}"
|
||||
logpush = true
|
||||
}
|
||||
|
||||
resource "cloudflare_worker_version" "worker" {
|
||||
account_id = var.cloudflare_account_id
|
||||
worker_id = cloudflare_worker.worker.id
|
||||
bindings = [
|
||||
{
|
||||
name = "EXAMPLE_BINDING"
|
||||
type = "plain_text"
|
||||
text = "example_value"
|
||||
}
|
||||
]
|
||||
compatibility_date = "2025-09-09"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
main_module = "index.js"
|
||||
modules = [
|
||||
{
|
||||
content_file = "${var.dist_dir}/${var.app_name}/index.js"
|
||||
content_type = "application/javascript+module"
|
||||
name = "index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
resource "cloudflare_workers_deployment" "worker" {
|
||||
account_id = var.cloudflare_account_id
|
||||
script_name = cloudflare_worker.worker.name
|
||||
strategy = "percentage"
|
||||
versions = [
|
||||
{
|
||||
percentage = 100
|
||||
version_id = cloudflare_worker_version.worker.id
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
data "cloudflare_zone" "immich_app" {
|
||||
filter = {
|
||||
name = "immich.cloud"
|
||||
}
|
||||
}
|
||||
|
||||
resource "cloudflare_workers_custom_domain" "worker" {
|
||||
account_id = var.cloudflare_account_id
|
||||
environment = "production"
|
||||
hostname = module.domain.fqdn
|
||||
service = cloudflare_worker.worker.name
|
||||
zone_id = data.cloudflare_zone.immich_app.zone_id
|
||||
}
|
||||
|
||||
module "domain" {
|
||||
source = "git::https://github.com/immich-app/devtools.git//tf/shared/modules/domain?ref=main"
|
||||
|
||||
app_name = var.app_name
|
||||
stage = var.stage
|
||||
env = var.env
|
||||
domain = "immich.cloud"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
locals {
|
||||
tf_state_postgres_conn_str = get_env("TF_VAR_tf_state_postgres_conn_str")
|
||||
}
|
||||
|
||||
remote_state {
|
||||
backend = "pg"
|
||||
|
||||
config = {
|
||||
conn_str = local.tf_state_postgres_conn_str
|
||||
}
|
||||
}
|
||||
|
||||
inputs = {
|
||||
tf_state_postgres_conn_str = local.tf_state_postgres_conn_str
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import js from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import eslintPluginUnicorn from 'eslint-plugin-unicorn';
|
||||
import globals from 'globals';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import typescriptEslint from 'typescript-eslint';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default typescriptEslint.config([
|
||||
eslintPluginUnicorn.configs.recommended,
|
||||
eslintPluginPrettierRecommended,
|
||||
js.configs.recommended,
|
||||
typescriptEslint.configs.recommended,
|
||||
{
|
||||
ignores: ['eslint.config.mjs', 'deployment/**', '**/.wrangler/**', 'dist'],
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
parser: typescriptEslint.parser,
|
||||
ecmaVersion: 5,
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
'unicorn/no-null': 'off',
|
||||
'unicorn/prefer-top-level-await': 'off',
|
||||
'unicorn/prefer-event-target': 'off',
|
||||
'unicorn/no-thenable': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
'unicorn/prefer-structured-clone': 'off',
|
||||
'unicorn/no-for-loop': 'off',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
'require-await': 'off',
|
||||
'@typescript-eslint/require-await': 'error',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
'object-shorthand': ['error', 'always'],
|
||||
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['.*'],
|
||||
message: 'Relative imports are not allowed.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.config.ts', '**/*.config.js', '**/*.config.mjs'],
|
||||
rules: {
|
||||
'no-restricted-imports': 'off',
|
||||
'unicorn/prefer-export-from': 'off',
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "immich-services",
|
||||
"version": "1.0.0",
|
||||
"description": "Immich Cloudflare Workers monorepo",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "pnpm -r build",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"lint:fix": "pnpm run lint -- --fix",
|
||||
"format": "prettier --check .",
|
||||
"format:fix": "prettier --write .",
|
||||
"test": "pnpm -r test",
|
||||
"check": "tsc --noEmit && pnpm -r typecheck"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/vitest-pool-workers": "^0.8.71",
|
||||
"@cloudflare/workers-types": "^4.20241127.0",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"@types/node": "^22.10.5",
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||
"@typescript-eslint/parser": "^8.19.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-unicorn": "^61.0.2",
|
||||
"globals": "^16.3.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-organize-imports": "^4.2.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.43.0",
|
||||
"vitest": "^3.0.0",
|
||||
"wrangler": "^4.35.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
|
||||
"engines": {
|
||||
"pnpm": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
Generated
+3487
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- 'apps/*'
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"module": "Node16",
|
||||
"lib": ["ES2021"],
|
||||
"moduleResolution": "Node16",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers", "@types/node"],
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"apps/*/src/**/*.ts",
|
||||
"apps/*/worker-configuration.d.ts",
|
||||
"*.config.ts",
|
||||
"apps/*/*.config.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "**/dist", "**/.wrangler"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
|
||||
|
||||
export default defineWorkersConfig({
|
||||
test: {
|
||||
poolOptions: {
|
||||
workers: {
|
||||
wrangler: { configPath: './wrangler.toml' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user