feat: data preview envs (#4)

This commit is contained in:
Jason Rasmussen
2024-10-04 16:14:55 +01:00
committed by GitHub
parent 4d7f75e9b3
commit a3c8fbae67
86 changed files with 11285 additions and 37 deletions
+21 -5
View File
@@ -13,11 +13,14 @@ concurrency:
jobs:
build:
name: Build
strategy:
matrix:
name: [backend, frontend]
name: Build ${{ matrix.name }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./web
working-directory: ./${{ matrix.name }}
steps:
- name: Checkout code
@@ -26,7 +29,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: './web/.nvmrc'
node-version-file: './${{ matrix.name }}/.nvmrc'
- name: Run npm install
run: npm ci
@@ -37,6 +40,19 @@ jobs:
- name: Upload build output
uses: actions/upload-artifact@v4
with:
name: build-output-web
path: web/build
name: build-output-${{ matrix.name }}
if-no-files-found: error
path: dist
retention-days: 1
merge:
name: Merge Artifacts
runs-on: ubuntu-latest
needs: build
steps:
- name: Merge Artifacts
uses: actions/upload-artifact/merge@v4
with:
name: build-output
retention-days: 1
+14 -10
View File
@@ -11,6 +11,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
parameters: ${{ steps.parameters.outputs.result }}
steps:
- if: ${{ github.event.workflow_run.conclusion == 'failure' }}
run: echo 'The triggering workflow failed' && exit 1
@@ -105,7 +106,7 @@ jobs:
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.find((artifact) => artifact.name == "build-output-web");
let matchArtifact = allArtifacts.data.artifacts.find((artifact) => artifact.name == "build-output");
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -116,39 +117,41 @@ jobs:
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/build-output.zip`, Buffer.from(download.data));
- name: Unzip artifact
run: unzip "${{ github.workspace }}/build-output.zip" -d "${{ github.workspace }}/build"
run: unzip "${{ github.workspace }}/build-output.zip" -d "${{ github.workspace }}/dist"
- name: Deploy App
env:
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name }}
TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
TF_VAR_app_url: data.immich.app
TF_VAR_app_name: immich-data
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
DIST_DIR: ${{ github.workspace }}/dist
ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
VMETRICS_API_TOKEN: ${{ secrets.VMETRICS_API_TOKEN }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: '0.58.12'
tofu_version: '1.7.1'
tg_dir: 'deployment/modules/cloudflare/static-pages'
tg_command: 'apply'
tg_dir: 'deployment/modules/cloudflare/'
tg_command: 'run-all apply'
- name: Deploy App Output
id: terraform-output
env:
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name }}
TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
TF_VAR_app_url: data.immich.app
TF_VAR_app_name: immich-data
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
DIST_DIR: ${{ github.workspace }}/dist
ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
VMETRICS_API_TOKEN: ${{ secrets.VMETRICS_API_TOKEN }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: '0.58.12'
tofu_version: '1.7.1'
tg_dir: 'deployment/modules/cloudflare/static-pages'
tg_dir: 'deployment/modules/cloudflare/'
tg_command: 'output -json'
- name: Output Cleaning
@@ -159,6 +162,7 @@ jobs:
- name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1
if: ${{ matrix.name == 'frontend' }}
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
@@ -169,7 +173,7 @@ jobs:
- name: Comment
uses: actions-cool/maintain-one-comment@v3
if: ${{ steps.parameters.outputs.event == 'pr' }}
if: ${{ matrix.name == 'frontend' && steps.parameters.outputs.event == 'pr' }}
with:
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
body: |
+6 -5
View File
@@ -12,18 +12,19 @@ jobs:
- name: Destroy Docs Subdomain
env:
TF_VAR_prefix_name: 'pr-${{ github.event.number }}'
TF_VAR_prefix_event_type: 'pr'
TF_VAR_app_url: data.immich.app
TF_VAR_app_name: immich-data
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name }}
TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
DIST_DIR: ${{ github.workspace }}/dist
ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
VMETRICS_API_TOKEN: ${{ secrets.VMETRICS_API_TOKEN }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: '0.58.12'
tofu_version: '1.7.1'
tg_dir: 'deployment/modules/cloudflare/static-pages'
tg_dir: 'deployment/modules/cloudflare/frontend'
tg_command: 'destroy'
- name: Comment
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./web
working-directory: ./frontend
steps:
- name: Checkout code
@@ -24,7 +24,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: './web/.nvmrc'
node-version-file: './frontend/.nvmrc'
- name: Run npm install
run: npm ci
+80
View File
@@ -0,0 +1,80 @@
# Custom rules
dist
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<files-pattern value="**/*.{js,ts,jsx,tsx,html,vue,json}" />
<option name="fix-on-save" value="true" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/data.immich.app.iml" filepath="$PROJECT_DIR$/.idea/data.immich.app.iml" />
</modules>
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
<option name="myRunOnSave" value="true" />
<option name="myFilesPattern" value="**/*.{js,ts,jsx,tsx,vue,astro,json,yml,yaml,md}" />
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+16
View File
@@ -0,0 +1,16 @@
module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: true,
sourceType: 'module',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint', 'eslint-plugin-import'],
rules: {
curly: 2,
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-explicit-any': 'off',
},
root: true,
};
+172
View File
@@ -0,0 +1,172 @@
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
# wrangler project
.dev.vars
.wrangler/
+1
View File
@@ -0,0 +1 @@
20.17.0
+17
View File
@@ -0,0 +1,17 @@
.DS_Store
node_modules
/build
/package
.env
.env.*
!.env.example
*.md
*.json
coverage
dist
worker-configuration.d.ts
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
+8
View File
@@ -0,0 +1,8 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"semi": true,
"organizeImportsSkipDestructiveCodeActions": true,
"plugins": ["prettier-plugin-organize-imports"]
}
+10305
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
{
"name": "worker",
"version": "0.0.0",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev",
"build": "wrangler deploy --dry-run --outdir=../dist/backend",
"test": "vitest",
"test:cov": "vitest --coverage",
"cf-typegen": "wrangler types",
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0",
"lint:fix": "npm run lint -- --fix",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"check": "tsc --noEmit"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.5.8",
"@cloudflare/workers-types": "^4.20240729.0",
"@types/express": "^4.17.21",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-istanbul": "^2.1.1",
"eslint": "^8.50.0",
"eslint-plugin-import": "^2.29.1",
"express": "^4.19.2",
"prettier-plugin-organize-imports": "^4.0.0",
"tsx": "^4.17.0",
"typescript": "^5.5.2",
"vite-node": "^2.1.1",
"vite-plugin-node-polyfills": "^0.22.0",
"wrangler": "^3.70.0"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.633.0",
"@influxdata/influxdb-client": "^1.34.0",
"fetch-retry": "^6.0.0",
"fflate": "^0.8.2",
"p-limit": "^6.1.0"
},
"volta": {
"node": "20.17.0"
}
}
+5
View File
@@ -0,0 +1,5 @@
interface WorkerEnv extends Omit<Env, 'ENVIRONMENT' | 'DEPLOYMENT_KEY'> {
ENVIRONMENT: string;
DEPLOYMENT_KEY: string;
VMETRICS_API_TOKEN: string;
}
+53
View File
@@ -0,0 +1,53 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { IMetricsRepository } from './interface';
import {
CloudflareDeferredRepository,
CloudflareMetricsRepository,
HeaderMetricsProvider,
InfluxMetricsProvider,
} from './repository';
enum Header {
PMTILES_DEPLOYMENT_KEY = 'PMTiles-Deployment-Key',
CACHE_CONTROL = 'Cache-Control',
ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin',
VARY = 'Vary',
CONTENT_TYPE = 'Content-Type',
CONTENT_ENCODING = 'Content-Encoding',
SERVER_TIMING = 'Server-Timing',
}
async function handleRequest(
request: Request<unknown, IncomingRequestCfProperties>,
env: WorkerEnv,
deferredRepository: CloudflareDeferredRepository,
metrics: IMetricsRepository,
) {
return new Response('Hello world!');
}
export default {
async fetch(request, env, ctx): Promise<Response> {
const workerEnv = env as WorkerEnv;
const deferredRepository = new CloudflareDeferredRepository(ctx);
const headerProvider = new HeaderMetricsProvider();
const influxProvider = new InfluxMetricsProvider(workerEnv.VMETRICS_API_TOKEN, env.ENVIRONMENT);
deferredRepository.defer(() => influxProvider.flush());
const metrics = new CloudflareMetricsRepository('data', request, [influxProvider, headerProvider]);
try {
const response = await metrics.monitorAsyncFunction({ name: 'handle_request' }, handleRequest)(
request,
workerEnv,
deferredRepository,
metrics,
);
response.headers.set(Header.SERVER_TIMING, headerProvider.getTimingHeader());
deferredRepository.runDeferred();
return response;
} catch (e) {
console.error(e);
return new Response('Internal Server Error', { status: 500 });
}
},
} satisfies ExportedHandler<Env>;
+25
View File
@@ -0,0 +1,25 @@
import { Metric } from './repository';
export interface IDeferredRepository {
defer(promise: AsyncFn): void;
runDeferred(): void;
}
export type AsyncFn = (...args: any[]) => Promise<any>;
export type Class = { new (...args: any[]): any };
export type Operation = { name: string; tags?: { [key: string]: string } };
export type Options = { monitorInvocations?: boolean; acceptedErrors?: Class[] };
export interface IMetricsRepository {
monitorAsyncFunction<T extends AsyncFn>(
operation: Operation,
call: T,
options?: Options,
): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>>;
push(metric: Metric): void;
}
export interface IMetricsProviderRepository {
pushMetric(metric: Metric): void;
flush(): void;
}
+33
View File
@@ -0,0 +1,33 @@
import { AsyncFn, Operation, Options } from './interface';
import { Metric } from './repository';
export function monitorAsyncFunction<T extends AsyncFn>(
operationPrefix: string,
operation: Operation,
call: T,
metricsWriteCallback: (metric: Metric) => void,
options: Options = {},
): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
const { name: operationName, tags = {} } = operation;
const { monitorInvocations = true, acceptedErrors = [] } = options;
const metric = Metric.create(`${operationPrefix}_${operationName}`);
metric.addTags(tags);
return async (...args: Parameters<T>) => {
if (monitorInvocations) {
metric.intField('invocation', 1);
}
return call(...args)
.catch((e) => {
if (!acceptedErrors || !acceptedErrors.some((acceptedError) => e instanceof acceptedError)) {
console.error(e, `${operationName}_errors`);
metric.intField('errors', 1);
}
throw e;
})
.finally(() => {
metric.durationField('duration');
metricsWriteCallback(metric);
});
};
}
+179
View File
@@ -0,0 +1,179 @@
import { Point } from '@influxdata/influxdb-client';
import {
AsyncFn,
IDeferredRepository,
IMetricsProviderRepository,
IMetricsRepository,
Operation,
Options,
} from './interface';
import { monitorAsyncFunction } from './monitor';
export class CloudflareDeferredRepository implements IDeferredRepository {
deferred: AsyncFn[] = [];
constructor(private ctx: ExecutionContext) {}
defer(call: AsyncFn): void {
this.deferred.push(call);
}
runDeferred() {
for (const call of this.deferred) {
this.ctx.waitUntil(call());
}
}
}
export class HeaderMetricsProvider implements IMetricsProviderRepository {
private _metrics: string[] = [];
constructor() {}
pushMetric(metric: Metric) {
for (const [label, { value, type }] of metric.fields) {
if (type === 'duration') {
const suffix = label === 'duration' ? '' : `_${label.replace('_duration', '')}`;
this._metrics.push(`${metric.name}${suffix};dur=${value}`);
}
}
}
getTimingHeader() {
return this._metrics.join(', ');
}
flush() {
console.log(this._metrics.join(', '));
}
}
export class InfluxMetricsProvider implements IMetricsProviderRepository {
private metrics: string[] = [];
constructor(
private influxApiToken: string,
private environment: string,
) {}
pushMetric(metric: Metric) {
const point = new Point(metric.name);
for (const [key, value] of metric.tags) {
point.tag(key, value);
}
for (const [key, { value, type }] of metric.fields) {
if (type === 'duration') {
point.intField(key, value);
} else if (type === 'int') {
point.intField(key, value);
}
}
const influxLineProtocol = point.toLineProtocol()?.toString();
if (influxLineProtocol) {
this.metrics.push(influxLineProtocol);
}
}
async flush() {
if (this.metrics.length === 0) {
return;
}
const metrics = this.metrics.join('\n');
if (this.environment === 'prod') {
const response = await fetch('https://cf-workers.monitoring.immich.cloud/write', {
method: 'POST',
body: this.metrics.join('\n'),
headers: {
Authorization: `Token ${this.influxApiToken}`,
},
});
await response.body?.cancel();
} else {
console.log(metrics);
}
}
}
export class Metric {
private _tags: Map<string, string> = new Map();
private _timestamp = performance.now();
private _fields = new Map<string, { value: any; type: 'duration' | 'int' }>();
private constructor(private _name: string) {}
static create(name: string) {
return new Metric(name);
}
get tags() {
return this._tags;
}
get timestamp() {
return this._timestamp;
}
get fields() {
return this._fields;
}
get name() {
return this._name;
}
addTag(key: string, value: string) {
this._tags.set(key, value);
return this;
}
addTags(tags: { [key: string]: string }) {
for (const [key, value] of Object.entries(tags)) {
this._tags.set(key, value);
}
return this;
}
durationField(key: string, duration?: number) {
this._fields.set(key, { value: duration ?? performance.now() - this._timestamp, type: 'duration' });
return this;
}
intField(key: string, value: number) {
this._fields.set(key, { value, type: 'int' });
return this;
}
}
export class CloudflareMetricsRepository implements IMetricsRepository {
private readonly defaultTags: { [key: string]: string };
constructor(
private operationPrefix: string,
request: Request<unknown, IncomingRequestCfProperties>,
private metricsProviders: IMetricsProviderRepository[],
) {
this.defaultTags = {
continent: request.cf?.continent ?? '',
colo: request.cf?.colo ?? '',
asOrg: request.cf?.asOrganization ?? '',
};
}
monitorAsyncFunction<T extends AsyncFn>(
operation: Operation,
call: T,
options: Options = {},
): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
operation = { ...operation, tags: { ...operation.tags, ...this.defaultTags } };
const callback = (metric: Metric) => {
for (const provider of this.metricsProviders) {
provider.pushMetric(metric);
}
};
return monitorAsyncFunction(this.operationPrefix, operation, call, callback, options);
}
push(metric: Metric) {
metric.addTags(this.defaultTags);
for (const provider of this.metricsProviders) {
provider.pushMetric(metric);
}
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "es2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": ["es2021"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react-jsx" /* Specify what JSX code is generated. */,
"module": "es2022" /* Specify what module code is generated. */,
"moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
"types": [
"@cloudflare/workers-types"
],
"resolveJsonModule": true /* Enable importing .json files */,
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
"checkJs": false /* Enable error reporting in type-checked JavaScript files. */,
"noEmit": true /* Disable emitting files from a compilation. */,
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"exclude": ["test"],
"include": ["worker-configuration.d.ts", "src/**/*.ts", "vitest.config.mts"]
}
+16
View File
@@ -0,0 +1,16 @@
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
export default defineWorkersConfig({
test: {
globals: true,
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.toml' },
},
},
// globalSetup: './test/globalSetup.ts',
coverage: {
provider: 'istanbul',
},
},
});
+8
View File
@@ -0,0 +1,8 @@
// Generated by Wrangler on Tue Sep 10 2024 12:58:38 GMT+0100 (British Summer Time)
// by running `wrangler types`
interface Env {
KV: KVNamespace;
DEPLOYMENT_KEY: '20240812';
ENVIRONMENT: 'prod';
}
+10
View File
@@ -0,0 +1,10 @@
#:schema node_modules/wrangler/config-schema.json
name = "tiles-dev"
main = "src/index.ts"
compatibility_date = "2024-07-29"
compatibility_flags = ["nodejs_compat"]
# Define environment variables for your Workers script
[vars]
DEPLOYMENT_KEY = "20240812"
ENVIRONMENT = "production"
@@ -0,0 +1,55 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.42.0"
constraints = "4.42.0"
hashes = [
"h1:+1iz3OcEEYBLeVS07cqRP5Kef0Y0XS8YEj/I1HH9J/A=",
"h1:/XZtSyok+I0hAHqj4hcA2RP4G9uvTABG4oMGC3vlS6o=",
"h1:5Z9Kfc5ufjqbLBXP9lZqQ11PAc4m6aTf/0M6miceOVQ=",
"h1:6cupVsiQundwTt+/lSuiR3s1s1H0JDN1H7USaF/+nDg=",
"h1:FasXNkaO7MNWy6GtuB/QFlnpQ7UuQQNotN1asY/r0Z8=",
"h1:G+5pvEHQlVkPXW6HK1twiWHOV/pavtSpQthZ0wb0HYM=",
"h1:HYhFNiRR1InZGjelCdQmg/KJHL2lFDlaWGSl2KA2fwk=",
"h1:LkMNEQUnYBAx8EFvl1BU/AVG1cs1Xi62kCvTVvYvDZ8=",
"h1:McPTXlnBSEzFGmVJHU7KyoBI3hJRh/eRcFTcijAfO/I=",
"h1:MoqiPtyXewRdNsevZUMqb0rXXpeLUVxNVEzux4AC7y4=",
"h1:RUgEZET04NprAFc4XAJwqqLiwNKaCTQEQEXtIZtNu1E=",
"h1:UCneKnBS53RlyJsdCqCCWrw19GE48tzTyO2u/V5u24c=",
"h1:aGbtjN8I2mLyW7aJ8KVRYuVdYeQCh3Ee8c3ixiijmjM=",
"h1:phTeKoT4aUmPXQO6d7ZbSQ8Cu41dhosACCEiDpRsL/I=",
"zh:1abb93dda2bf73b1656dc63673c752642bfa4c31c8ca1e83cdd278f42fa121c1",
"zh:2b9c90fe4fffaadfeccd0f457bd1354ba2dba920c5525748d8f8f24656d6c7bd",
"zh:710a0cf84406df09705115a62bd6a418188a3b884f615fe7122a6fc51ec59bc0",
"zh:7875b38b281d17a24d89116b33f92b8b24292be160a2c618874a0f674171bd34",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:ad28c94908c336ca97feabb02734a2c115eddaa5f4a625f00bfafb3ac65646e4",
"zh:aee7861d44cf1a71a4846e1ffda20992259838ee600ca79d3abc80b43f4ff0f0",
"zh:b1327e8ea04f0df5efc2b8848c358bf0d256716ad5372f91f5960296a1f68677",
"zh:b9ec2310feffef38c1488bed038ad7942c54f930f2166a600b5fc850e377bddf",
"zh:d04178727d7157aae8eb66b4f7318338c89cd685b53c67f5ce41de4160c2d484",
"zh:d76c4bb5a5bfed710fcc8a1d919172cbfcbda0cec177f1d2cb44aff82a862a32",
"zh:ddf3c126b5c98267240f960f8fed381115675793e5b86dd2cbdebeb628efb0cb",
"zh:e3efb3b9409626f15931a81edaeb96e4baf462a24b32dac9d97cc36d1fef1f01",
"zh:e925c963cddf5778829f0b26f72ad8bcbc1aed3510cfa0bd5a9e7cbf4c64a6e8",
"zh:ff78a903b2432fbff7b04e84a23c71425589bad7a4b846e08bc131268b80900f",
]
}
provider "registry.opentofu.org/hashicorp/http" {
version = "3.4.5"
hashes = [
"h1:IkHNZ09hqg1TtLrUqUHIqoKXbNJE69dk917niblQf3U=",
"zh:055a4431d33bb89b9848193152433eaead7cc2e6746d3436a5922419de2112bf",
"zh:0bfabafea9f5e36802fcfc5a800831ec1767d896af889abc610014d02b09bdc2",
"zh:300b4983fe1b43bd0a7dac1f94b30b3814f11c824224dd83fb45a521c02cea60",
"zh:68f6958314ca5dc0868be70e37ec123b99b8828aa49f27fd2fdd13df05d31ab1",
"zh:c29f098a597250adc2a7d9f99acbce3c9e07d37f1c5cfded5df4309738cf613c",
"zh:c33607397f9c9302c0cd797c8b7484c9c6cfa09c3489d4b55af17df20b204368",
"zh:d519ca364a224110428b390ee06e963a3ec4dfdd1ac816c9f32e647567957cf5",
"zh:e4a9c7c0ac31a0192362ef43449390cdf00d2cf6f13061ef730b177eaf00ac45",
"zh:f25223c062f274d8f89bb96017e73586030a205bc91cdad266a9954d0def2a23",
"zh:fd4dc824ebae2f3a66318df364bec83b88e9a52e7f66b00dafa29a796d9a94ab",
]
}
@@ -0,0 +1,11 @@
terraform {
backend "pg" {}
required_version = "~> 1.7"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.42.0"
}
}
}
@@ -0,0 +1,3 @@
provider "cloudflare" {
api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_account
}
@@ -0,0 +1,8 @@
data "terraform_remote_state" "api_keys_state" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_api_keys"
}
}
@@ -0,0 +1,36 @@
terraform {
source = "."
extra_arguments custom_vars {
commands = get_terraform_commands_that_need_vars()
}
}
include "cloudflare" {
path = find_in_parent_folders("cloudflare.hcl")
}
include "root" {
path = find_in_parent_folders("root.hcl")
}
locals {
dist_dir = get_env("DIST_DIR")
vmetrics_api_token = get_env("VMETRICS_API_TOKEN")
env = get_env("ENVIRONMENT")
}
inputs = {
dist_dir = local.dist_dir
vmetrics_api_token = local.vmetrics_api_token
env = local.env
}
remote_state {
backend = "pg"
config = {
conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
schema_name = "${local.env}_cloudflare_data_worker"
}
}
@@ -0,0 +1,5 @@
variable "cloudflare_account_id" {}
variable "tf_state_postgres_conn_str" {}
variable "dist_dir" {}
variable "vmetrics_api_token" {}
variable "env" {}
@@ -0,0 +1,47 @@
data "cloudflare_zone" "immich_cloud" {
name = "immich.cloud"
}
resource "cloudflare_workers_script" "data_api" {
account_id = var.cloudflare_account_id
name = "data-api-${var.env}"
content = file("${var.dist_dir}/backend/index.js")
module = true
plain_text_binding {
name = "ENVIRONMENT"
text = var.env
}
secret_text_binding {
name = "VMETRICS_API_TOKEN"
text = var.vmetrics_api_token
}
compatibility_date = "2024-07-29"
compatibility_flags = ["nodejs_compat"]
}
data "http" "data_api_deployments" {
depends_on = [cloudflare_workers_script.data_api]
url = "https://api.cloudflare.com/client/v4/accounts/${var.cloudflare_account_id}/workers/scripts/${cloudflare_workers_script.data_api.name}/deployments"
request_headers = {
Authorization = "Bearer ${data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_account}"
}
}
locals {
data_api_version_id = split("-", jsondecode(data.http.data_api_deployments.response_body).result.deployments[0].versions[0].version_id)[0]
}
output "data_api_preview_url" {
value = "https://${local.data_api_version_id}-${cloudflare_workers_script.data_api.name}.immich.workers.dev"
}
resource "cloudflare_workers_route" "data_api_prod" {
count = var.env == "prod" ? 1 : 0
pattern = "data.immich.app/api*"
script_name = cloudflare_workers_script.data_api.name
zone_id = data.cloudflare_zone.immich_cloud.zone_id
}
@@ -0,0 +1,9 @@
locals {
cloudflare_account_id = get_env("CLOUDFLARE_ACCOUNT_ID")
cloudflare_api_token = get_env("CLOUDFLARE_API_TOKEN")
}
inputs = {
cloudflare_account_id = local.cloudflare_account_id
cloudflare_api_token = local.cloudflare_api_token
}
@@ -1,6 +1,6 @@
resource "cloudflare_pages_domain" "immich_app_branch_domain" {
account_id = var.cloudflare_account_id
project_name = data.terraform_remote_state.cloudflare_account.outputs.static_pages_project_names[var.app_url]
project_name = data.terraform_remote_state.cloudflare_account.outputs.static_pages_project_names["data.immich.app"]
domain = local.domain
}
@@ -9,7 +9,7 @@ resource "cloudflare_record" "immich_app_branch_subdomain" {
proxied = true
ttl = 1
type = "CNAME"
value = local.is_main ? data.terraform_remote_state.cloudflare_account.outputs.static_pages_project_subdomains[var.app_url] : "${replace(var.prefix_name, "/\\/|\\./", "-")}.${data.terraform_remote_state.cloudflare_account.outputs.static_pages_project_subdomains[var.app_url]}"
content = local.is_main ? data.terraform_remote_state.cloudflare_account.outputs.static_pages_project_subdomains["data.immich.app"] : "${replace(var.prefix_name, "/\\/|\\./", "-")}.${data.terraform_remote_state.cloudflare_account.outputs.static_pages_project_subdomains["data.immich.app"]}"
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
}
@@ -18,7 +18,7 @@ output "immich_app_branch_subdomain" {
}
output "immich_app_branch_pages_hostname" {
value = cloudflare_record.immich_app_branch_subdomain.value
value = cloudflare_record.immich_app_branch_subdomain.content
}
output "pages_project_name" {
@@ -1,5 +1,5 @@
locals {
is_main = var.prefix_name == "main"
domain_prefix = !local.is_main && contains(["branch", "pr"], var.prefix_event_type) ? "${var.prefix_name}.preview." : ""
domain = "${local.domain_prefix}${var.app_url}"
domain = "${local.domain_prefix}data.immich.app"
}
@@ -6,13 +6,20 @@ terraform {
}
}
include {
path = find_in_parent_folders("state.hcl")
include "cloudflare" {
path = find_in_parent_folders("cloudflare.hcl")
}
include "root" {
path = find_in_parent_folders("root.hcl")
}
locals {
prefix_name = get_env("TF_VAR_prefix_name")
app_name = replace(get_env("TF_VAR_app_name"), "-", "_")
}
dependencies {
paths = ["../backend"]
}
remote_state {
@@ -20,6 +27,6 @@ remote_state {
config = {
conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
schema_name = "prod_cloudflare_immich_app_${local.app_name}_${local.prefix_name}"
schema_name = "prod_cloudflare_immich_app_data_${local.prefix_name}"
}
}
@@ -3,4 +3,3 @@ variable "tf_state_postgres_conn_str" {}
variable "prefix_name" {}
variable "prefix_event_type" {}
variable "app_url" {}
@@ -1,7 +1,4 @@
locals {
cloudflare_account_id = get_env("CLOUDFLARE_ACCOUNT_ID")
cloudflare_api_token = get_env("CLOUDFLARE_API_TOKEN")
tf_state_postgres_conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
}
@@ -14,7 +11,5 @@ remote_state {
}
inputs = {
cloudflare_account_id = local.cloudflare_account_id
cloudflare_api_token = local.cloudflare_api_token
tf_state_postgres_conn_str = local.tf_state_postgres_conn_str
}
-1
View File
@@ -4,7 +4,6 @@ node_modules
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
View File
View File
View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Before

Width:  |  Height:  |  Size: 594 KiB

After

Width:  |  Height:  |  Size: 594 KiB

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

@@ -10,6 +10,7 @@ const config = {
kit: {
adapter: adapter({
fallback: 'index.html',
pages: '../dist/frontend',
}),
},
};