Compare commits

..

63 Commits

Author SHA1 Message Date
Alex Yang
a52fc54d80 v0.7.0-canary.25 2023-06-30 16:27:59 +08:00
Alex Yang
524c342b5e chore: bump blocksuite to '0.0.0-20230630081054-55a25248-nightly' (#2944) 2023-06-30 16:27:38 +08:00
regischen
f4fc084a0a fix(web): migrate connector (#2941)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-06-30 16:11:04 +08:00
Alex Yang
38a2aa9d17 build(electron): use nx (#2942) 2023-06-30 16:10:35 +08:00
Peng Xiao
9e90242ddb fix: disable sqlite blob storage (#2943) 2023-06-30 16:09:43 +08:00
Alex Yang
fd0c1da608 fix(cli): run dev-web crash 2023-06-30 15:58:13 +08:00
Alex Yang
68c4fccf98 ci: cancel previous build (#2794) 2023-06-30 07:39:27 +00:00
Alex Yang
3c93f4162d build: remove unused package (#2937) 2023-06-30 07:09:12 +00:00
Alex Yang
b6c314e180 refactor(cli): use typescript (#2938) 2023-06-30 06:58:57 +00:00
Alex Yang
62b465a889 ci: build infra code before build layers 2023-06-30 15:20:36 +08:00
3720
9d0db78f64 feat: support for view management (#2892) 2023-06-30 05:40:00 +00:00
Fangdun Tsai
d3393cb0fc feat: expose clipboard apis (#2932) 2023-06-30 04:47:30 +00:00
Alex Yang
79cded302f chore: bump blocksuite to 0.0.0-20230629103121-76e6587d-nightly (#2931) 2023-06-30 04:36:56 +00:00
Alex Yang
53d90a11de chore: tag deprecated files (#2936) 2023-06-30 04:01:14 +00:00
Alex Yang
271ad57160 feat: special ip address 'localhost' (#2935) 2023-06-30 03:54:24 +00:00
Alex Yang
4adbe64a54 fix(web): disable notification center (#2934) 2023-06-30 03:29:05 +00:00
DarkSky
50a8a147fd ci: make helm release only on bump version (#2928) 2023-06-30 02:02:46 +00:00
Ikko Eltociear Ashimine
eaea8e9368 refactor: fix typo in notification-center/index.tsx (#2929) 2023-06-30 09:55:09 +08:00
Hyden Liu
9873baae9f fix: z-index on app sidebar (#2761)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-06-30 01:14:44 +00:00
xiaodong zuo
bc3ce7395e feat: export page as file (#2923) 2023-06-29 21:58:02 +00:00
Alex Yang
8a7908c692 fix(electron): window only ui (#2926) 2023-06-29 16:15:44 +00:00
LongYinan
8021efd81a build: affine Node.js server charts (#2895) 2023-06-29 14:02:46 +00:00
Qi
d7fcad2d0d feat: add and modify test case for new settings modal (#2925) 2023-06-29 12:54:45 +00:00
Alex Yang
b1d2d77263 docs: set nodejs version to 18.16.1 2023-06-29 20:00:57 +08:00
Alex Yang
2c772bd81b v0.7.0-canary.24 2023-06-29 18:50:48 +08:00
JimmFly
7f00011542 chore: update changelog link and remove obsolete changelog components (#2918) 2023-06-29 10:19:26 +00:00
Alex Yang
f76d8b8818 chore: bump blocksuite to 0.0.0-20230629084521-542de4e8-nightly (#2921) 2023-06-29 09:42:47 +00:00
Alex Yang
1d6b39dec9 ci: allow codecov upload failure (#2922) 2023-06-29 09:39:16 +00:00
Qi
5cfdf6c7e2 fix: a serise of ui issues of new setting (#2920)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-06-29 09:25:42 +00:00
Alex Yang
8410d83744 refactor: rootWorkspacesMetadataAtom loading logic (#2882) 2023-06-29 08:48:12 +00:00
DarkSky
8a2dac9718 fix: incorrect formatting (#2917) 2023-06-29 08:25:43 +00:00
JimmFly
5ad2908760 chore: update translation (#2916)
Co-authored-by: zuozijian3720 <zuozijian1994@gmail.com>
2023-06-29 08:20:25 +00:00
Alex Yang
5b8771485e docs: add apps/README.md 2023-06-29 16:07:30 +08:00
Alex Yang
ed8480caf0 ci: split migration test 2023-06-29 15:11:16 +08:00
Alex Yang
42ef3c0fc2 test: migration test in real world (#2885) 2023-06-29 06:50:26 +00:00
Alex Yang
e08ee9b7ff ci: add prettier format check (#2908) 2023-06-29 04:13:35 +00:00
liuyi
2c95bfcc3d feat(storage): binding jwst storage to node (#2808) 2023-06-29 01:45:45 +00:00
Alex Yang
86616e152d build: disable sqlite provider in canary 2023-06-29 10:00:41 +08:00
Peng Xiao
b1f478ee5e fix: updater color updates (#2913) 2023-06-28 17:21:07 +00:00
DarkSky
6b0f9fbdad feat: add deployment guide & fix pod label (#2912) 2023-06-28 17:12:23 +00:00
Alex Yang
da3f2b784a ci: fix output variable 2023-06-29 01:20:35 +08:00
Alex Yang
acb140ab78 v0.7.0-canary.23 2023-06-29 00:40:50 +08:00
Alex Yang
0b74bd9bfe ci: use production environment 2023-06-29 00:40:50 +08:00
Alex Yang
acfc030d16 ci: fix package version output 2023-06-29 00:40:50 +08:00
Alex Yang
d0d04ce376 v0.7.0-canary.22 2023-06-29 00:27:17 +08:00
Alex Yang
2250f42d2a ci: fix tag version 2023-06-29 00:26:48 +08:00
Alex Yang
887434fea4 v0.7.0-canary.21 2023-06-29 00:23:06 +08:00
Alex Yang
9b817c4b79 ci: automatically build canary release (#2911) 2023-06-28 15:53:32 +00:00
Alex Yang
ea03bbfb2d ci: add codeql check to merge group (#2909) 2023-06-28 15:07:27 +00:00
Qi
db40cd35c6 feat: migrate workspace setting with new design to setting modal (#2900)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-06-28 14:45:33 +00:00
Alex Yang
aabac9e921 chore: bump typescript version (#2906) 2023-06-28 12:57:33 +00:00
Alex Yang
0a91c41e0a chore: codesandbox setup (#2907) 2023-06-28 12:32:56 +00:00
DarkSky
d6addc0d0b docs: improve helm ci & document (#2902) 2023-06-28 12:30:02 +00:00
Alex Yang
91d3b76be5 refactor(storybook): move to apps folder (#2901) 2023-06-28 12:29:52 +00:00
Alex Yang
3eed009270 feat: add rule 'sonarjs/no-identical-functions' (#2905) 2023-06-28 12:29:12 +00:00
Alex Yang
bc14d54cfa chore: update pre-commit hook (#2904) 2023-06-28 11:24:37 +00:00
Alex Yang
5496969e58 refactor: environment setup (#2898)
Co-authored-by: Simon He <57086651+Simon-He95@users.noreply.github.com>
2023-06-28 11:19:19 +00:00
Alex Yang
80c2a78273 fix(web): bypass adapter list error (#2903) 2023-06-28 11:06:13 +00:00
Alex Yang
92f378aefc test(server): watch mode (#2893) 2023-06-28 10:00:06 +00:00
Alex Yang
877ceee698 ci: enable merge group (#2899) 2023-06-28 09:56:02 +00:00
Alex Yang
7960b6a22e feat: update migration test page (#2871) 2023-06-28 16:46:08 +08:00
Alex Yang
fa45d8a718 build: unify build flags (#2891) 2023-06-28 16:45:05 +08:00
Alex Yang
87574c9993 build: fix i18n output (#2896) 2023-06-28 16:40:41 +08:00
355 changed files with 13334 additions and 2910 deletions

20
.codesandbox/task.json Normal file
View File

@@ -0,0 +1,20 @@
{
"$schema": "https://codesandbox.io/schemas/tasks.json",
"setupTasks": [
{
"name": "Install Dependencies",
"command": "yarn install"
}
],
"tasks": {
"start-web": {
"name": "Start Web",
"command": "yarn nx dev @affine/web --port 8080",
"runAtStart": true,
"preview": {
"port": 8080
}
}
}
}

View File

@@ -9,6 +9,7 @@
"server",
"web",
"docs",
"storybook",
"component",
"workspace",
"env",
@@ -21,7 +22,7 @@
"templates",
"y-indexeddb",
"debug",
"theme"
"storage"
]
]
}

View File

@@ -41,6 +41,7 @@ const allPackages = [
'apps/web',
'apps/server',
'apps/electron',
'apps/storybook',
'plugins/copilot',
'plugins/bookmark-block',
];
@@ -64,6 +65,7 @@ const config = {
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:@typescript-eslint/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
@@ -157,6 +159,7 @@ const config = {
'sonarjs/no-duplicated-branches': 'error',
'sonarjs/no-collection-size-mischeck': 'error',
'sonarjs/no-useless-catch': 'error',
'sonarjs/no-identical-functions': 'error',
},
overrides: [
{

View File

@@ -27,11 +27,11 @@ runs:
.cargo-cache
target/${{ inputs.target }}
key: stable-${{ inputs.target }}-cargo-cache
- name: Build
if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }}
shell: bash
run: yarn nx build @affine/native --target ${{ inputs.target }}
run: |
yarn nx build @affine/native --target ${{ inputs.target }}
env:
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
@@ -41,10 +41,10 @@ runs:
with:
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build -e NX_CLOUD_ACCESS_TOKEN=${{ inputs.nx_token }}
run: >-
export CC=x86_64-unknown-linux-gnu-gcc &&
export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc &&
yarn nx build @affine/native --target ${{ inputs.target }} &&
run: |
export CC=x86_64-unknown-linux-gnu-gcc
export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc
yarn nx build @affine/native --target ${{ inputs.target }}
chmod -R 777 node_modules/.cache
- name: Build
@@ -53,6 +53,6 @@ runs:
with:
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build -e NX_CLOUD_ACCESS_TOKEN=${{ inputs.nx_token }}
run: >-
run: |
yarn nx build @affine/native --target ${{ inputs.target }}
chmod -R 777 node_modules/.cache

31
.github/actions/setup-rust/action.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: 'AFFiNE Rust setup'
description: 'Rust setup, including cache configuration'
inputs:
target:
description: 'Cargo target'
required: true
toolchain:
description: 'Rustup toolchain'
required: false
default: 'stable'
runs:
using: 'composite'
steps:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ inputs.toolchain }}
targets: ${{ inputs.target }}
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-cache-${{ runner.os }}-${{ inputs.toolchain }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-cache-${{ runner.os }}-${{ inputs.toolchain }}-

View File

@@ -3,7 +3,7 @@ server {
root /app/dist;
location / {
try_files $uri $uri/index.html $uri.html =404;;
try_files $uri $uri/index.html $uri.html =404;
}
error_page 404 /404.html;

1
.github/helm/affine-cloud/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
charts/

View File

@@ -20,4 +20,4 @@
.project
.idea/
*.tmproj
.vscode/
.vscode/

View File

@@ -3,8 +3,8 @@ name: affine-cloud
description: A Helm chart for AFFiNE Cloud
type: application
version: 0.6.0
appVersion: '0.6.0'
version: 0.6.1
appVersion: '0.6.1'
dependencies:
- name: postgresql

30
.github/helm/affine-cloud/readme.md vendored Normal file
View File

@@ -0,0 +1,30 @@
# Helm Chart Configuration
The following table lists the configurable parameters of this Helm chart and their default values.
## AFFiNE Cloud Server parameters
| Parameter | Description | Default |
| ------------------------------ | -------------------------------------------------- | ------------------ |
| `affineCloud.tag` | The Docker tag of the AffineCloud image to be used | `'nightly-latest'` |
| `affineCloud.resources.cpu` | The CPU resources allocated for AffineCloud | `'250m'` |
| `affineCloud.resources.memory` | The memory resources allocated for AffineCloud | `'0.5Gi'` |
| `affineCloud.signKey` | The key used to sign the JWT tokens | `'c2VjcmV0'` |
| `affineCloud.service.type` | The type of the Kubernetes service | `'ClusterIP'` |
| `affineCloud.service.port` | The port of the Kubernetes service | `'http'` |
| `affineCloud.mail.account` | The email account used to send emails | `''` |
| `affineCloud.mail.password` | The password of the email account | `''` |
## PostgreSQL parameters
| Parameter | Description | Default |
| -------------------------------------------- | ------------------------------------------------------------------------------------- | ------------ |
| `postgresql.auth.username` | Username for the PostgreSQL database | `'affine'` |
| `postgresql.auth.password` | Password for the PostgreSQL database. Please change this for production environments. | `'password'` |
| `postgresql.auth.database` | The name of the default database that will be created on image startup | `'affine'` |
| `postgresql.primary.resources.limits.cpu` | The CPU resources allocated for the PostgreSQL primary node | `'500m'` |
| `postgresql.primary.resources.limits.memory` | The memory resources allocated for the PostgreSQL primary node | `'0.5Gi'` |
For more postgres parameters, please refer to: https://artifacthub.io/packages/helm/bitnami/postgresql
Please note that for the `postgresql.auth.password`, you should provide your own password for production environments. The default value is provided only for demonstration purposes.

View File

@@ -0,0 +1,51 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "affine-cloud.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "affine-cloud.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "affine-cloud.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "affine-cloud.labels" -}}
helm.sh/chart: {{ include "affine-cloud.chart" . }}
{{ include "affine-cloud.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "affine-cloud.selectorLabels" -}}
app.kubernetes.io/name: {{ include "affine-cloud.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

View File

@@ -1,12 +1,14 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: affine-cloud
name: "{{ include "affine-cloud.fullname" . }}"
labels:
{{- include "affine-cloud.labels" . | nindent 4 }}
spec:
replicas: 1
selector:
matchLabels:
app: affine-cloud
{{- include "affine-cloud.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate:
@@ -14,7 +16,7 @@ spec:
template:
metadata:
labels:
app: affine-cloud
{{- include "affine-cloud.selectorLabels" . | nindent 8 }}
spec:
restartPolicy: Always
containers:
@@ -28,9 +30,9 @@ spec:
- name: PG_DATABASE
value: "{{ .Values.postgresql.auth.database }}"
- name: PG_HOST
value: "{{ .Release.Name }}-postgresql"
value: "{{ .Values.postgresql.fullnameOverride | default (printf "%s-postgresql" .Release.Name) }}"
- name: DATABASE_URL
value: "{{ default "postgresql://$(PG_USER):$(PG_PASS)@$(PG_HOST)/$(PG_DATABASE)" .Values.affineCloud.databaseUrl }}"
value: "{{ .Values.affineCloud.databaseUrl | default "postgresql://$(PG_USER):$(PG_PASS)@$(PG_HOST)/$(PG_DATABASE)" }}"
envFrom:
- secretRef:
name: affine-cloud-secret

View File

@@ -4,7 +4,6 @@ metadata:
name: affine-cloud-secret
type: Opaque
data:
# only for demo, please modify it at prod env
SIGN_KEY: TUFtdFdzQTJhdGJuem01TA==
# MAIL_ACCOUNT: XXXX
# MAIL_PASSWORD: XXXX
SIGN_KEY: "{{ .Values.affineCloud.signKey }}"
MAIL_ACCOUNT: "{{ .Values.affineCloud.mail.account }}"
MAIL_PASSWORD: "{{ .Values.affineCloud.mail.password }}"

View File

@@ -1,13 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: affine-cloud
name: "{{ include "affine-cloud.fullname" . }}"
labels:
{{- include "affine-cloud.labels" . | nindent 4 }}
spec:
type: ClusterIP
selector:
app: affine-cloud
type: "{{ .Values.affineCloud.service.type }}"
ports:
- name: affine-cloud
- name: http
protocol: TCP
port: 3000
port: {{ .Values.affineCloud.service.port }}
targetPort: 3000
selector:
{{- include "affine-cloud.selectorLabels" . | nindent 4 }}

View File

@@ -1,13 +1,22 @@
affineCloud:
tag: 'nightly-latest'
tag: 'canary-5e0d5e0cc65ea46f326fdde12658bfac59b38c9f-0949'
# databaseUrl: 'postgresql://affine:password@affine-cloud-postgresql:5432/affine'
signKey: TUFtdFdzQTJhdGJuem01TA==
mail:
account: ''
password: ''
service:
type: ClusterIP
port: 80
resources:
cpu: '250m'
memory: 0.5Gi
postgresql:
fullnameOverride: tcp-postgresql
auth:
# only for demo, please modify it at prod env
username: affine
password: XJYMLnuBJS27a2du
password: password
database: affine
primary:
initdb:

23
.github/helm/affine/.helmignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

6
.github/helm/affine/Chart.yaml vendored Normal file
View File

@@ -0,0 +1,6 @@
apiVersion: v2
name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: '0.7.0-canary.18'

View File

@@ -0,0 +1,6 @@
apiVersion: v2
name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: '0.7.0-canary.18'

View File

@@ -0,0 +1,16 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "graphql.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "graphql.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "graphql.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "graphql.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -0,0 +1,132 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "graphql.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "graphql.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "graphql.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "graphql.labels" -}}
helm.sh/chart: {{ include "graphql.chart" . }}
{{ include "graphql.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "graphql.selectorLabels" -}}
app.kubernetes.io/name: {{ include "graphql.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "graphql.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "graphql.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{- define "jwt.key" -}}
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.jwt.secretName -}}
{{- if and $secret $secret.data.private -}}
{{/*
Reusing existing secret data
*/}}
key: {{ $secret.data.private }}
{{- else -}}
{{/*
Generate new data
*/}}
key: {{ genPrivateKey "ecdsa" | b64enc }}
{{- end -}}
{{- end -}}
{{- define "objectStorage.r2" -}}
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.objectStorage.r2.secretName -}}
{{- if $secret -}}
{{/*
Reusing existing secret data
*/}}
accountId: {{ $secret.data.accountId }}
accessKeyId: {{ $secret.data.accessKeyId }}
secretAccessKey: {{ $secret.data.secretAccessKey }}
bucket: {{ $secret.data.bucket }}
{{- else -}}
{{/*
Generate new data
*/}}
accountId: {{ .Values.app.objectStorage.r2.accountId | b64enc }}
accessKeyId: {{ .Values.app.objectStorage.r2.accessKeyId | b64enc }}
secretAccessKey: {{ .Values.app.objectStorage.r2.secretAccessKey | b64enc }}
bucket: {{ .Values.app.objectStorage.r2.bucket | b64enc }}
{{- end -}}
{{- end -}}
{{- define "objectStorage.oauth.google" -}}
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.oauth.google.secretName -}}
{{- if $secret -}}
{{/*
Reusing existing secret data
*/}}
clientId: {{ $secret.data.clientId }}
clientSecret: {{ $secret.data.clientSecret }}
{{- else -}}
{{/*
Generate new data
*/}}
clientId: "{{ .Values.app.oauth.google.clientId | b64enc }}"
clientSecret: "{{ .Values.app.oauth.google.clientSecret | b64enc }}"
{{- end -}}
{{- end -}}
{{- define "objectStorage.oauth.github" -}}
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.oauth.github.secretName -}}
{{- if $secret -}}
{{/*
Reusing existing secret data
*/}}
clientId: {{ $secret.data.clientId }}
clientSecret: {{ $secret.data.clientSecret }}
{{- else -}}
{{/*
Generate new data
*/}}
clientId: "{{ .Values.app.oauth.github.clientId | b64enc }}"
clientSecret: "{{ .Values.app.oauth.github.clientSecret | b64enc }}"
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,126 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "graphql.fullname" . }}
labels:
{{- include "graphql.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "graphql.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "graphql.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "graphql.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AUTH_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.app.jwt.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: DATABSE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.database.user }}:$(DATABSE_PASSWORD)@{{ .Values.database.url }}:{{ .Values.database.port }}/{{ .Values.database.name }}
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_SUB_PATH
value: "{{ .Values.app.path }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: ENABLE_R2_OBJECT_STORAGE
value: "{{ .Values.app.objectStorage.r2.enabled }}"
{{ if .Values.app.objectStorage.r2.enabled }}
- name: R2_OBJECT_STORAGE_ACCOUNT_ID
valueFrom:
secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}"
key: accountId
- name: R2_OBJECT_STORAGE_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}"
key: accessKeyId
- name: R2_OBJECT_STORAGE_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}"
key: secretAccessKey
- name: R2_OBJECT_STORAGE_BUCKET
valueFrom:
secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}"
key: bucket
{{ end }}
{{ if .Values.app.oauth.google.enabled }}
- name: OAUTH_GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.google.secretName }}"
key: clientId
- name: OAUTH_GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.google.secretName }}"
key: clientSecret
{{ end }}
{{ if .Values.app.oauth.github.enabled }}
- name: OAUTH_GITHUB_CLIENT_ID
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.github.secretName }}"
key: clientId
- name: OAUTH_GITHUB_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.github.secretName }}"
key: clientSecret
{{ end }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Values.app.jwt.secretName }}"
type: Opaque
data:
{{- ( include "jwt.key" . ) | indent 2 -}}

View File

@@ -0,0 +1,34 @@
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "graphql.fullname" . }}-database-migration
labels:
{{- include "graphql.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-1"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["yarn", "prisma", "migrate", "deploy"]
env:
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: DATABSE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.database.user }}:$(DATABSE_PASSWORD)@{{ .Values.database.url }}:{{ .Values.database.port }}/{{ .Values.database.name }}
resources:
requests:
cpu: '100m'
memory: '200Mi'
restartPolicy: Never
backoffLimit: 1

View File

@@ -0,0 +1,10 @@
{{- if .Values.app.oauth.github.enabled -}}
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Values.app.oauth.github.secretName }}"
type: Opaque
data:
{{- ( include "objectStorage.oauth.github" . ) | indent 2 -}}
{{- end }}

View File

@@ -0,0 +1,10 @@
{{- if .Values.app.oauth.google.enabled -}}
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Values.app.oauth.google.secretName }}"
type: Opaque
data:
{{- ( include "objectStorage.oauth.google" . ) | indent 2 -}}
{{- end }}

View File

@@ -0,0 +1,9 @@
{{- if .Values.app.objectStorage.r2.enabled -}}
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Values.app.objectStorage.r2.secretName }}"
type: Opaque
data:
{{- ( include "objectStorage.r2" . ) | indent 2 -}}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "graphql.fullname" . }}
labels:
{{- include "graphql.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "graphql.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "graphql.serviceAccountName" . }}
labels:
{{- include "graphql.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "graphql.fullname" . }}-test-connection"
labels:
{{- include "graphql.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "graphql.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,69 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine-graphql
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
# map to NODE_ENV environment variable
env: 'production'
database:
user: 'postgres'
url: 'pg-postgresql'
port: '5432'
name: 'affine'
app:
# AFFINE_SERVER_SUB_PATH
path: ''
# AFFINE_SERVER_HOST
host: '0.0.0.0'
jwt:
secretName: jwt-private-key
# base64 encoded ecdsa private key
privateKey: ''
objectStorage:
r2:
enabled: false
secretName: r2
accountId: ''
accessKeyId: ''
secretAccessKey: ''
bucket: ''
oauth:
google:
enabled: false
secretName: oauth-google
clientId: ''
clientSecret: ''
github:
enabled: false
secretName: oauth-github
clientId: ''
clientSecret: ''
serviceAccount:
create: true
annotations: {}
name: 'affine-graphql'
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
limits:
cpu: '2000m'
memory: 4Gi
requests:
cpu: '1000m'
memory: 2Gi
probe:
initialDelaySeconds: 20
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,6 @@
apiVersion: v2
name: web
description: A Helm chart for Kubernetes
type: application
version: 0.0.0
appVersion: "0.7.0-canary.18"

View File

@@ -0,0 +1,16 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "web.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "web.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "web.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "web.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "web.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "web.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "web.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "web.labels" -}}
helm.sh/chart: {{ include "web.chart" . }}
{{ include "web.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "web.selectorLabels" -}}
app.kubernetes.io/name: {{ include "web.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "web.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "web.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,57 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "web.fullname" . }}
labels:
{{- include "web.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "web.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "web.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "web.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "web.fullname" . }}
labels:
{{- include "web.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "web.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "web.serviceAccountName" . }}
labels:
{{- include "web.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "web.fullname" . }}-test-connection"
labels:
{{- include "web.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "web.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,37 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine-front
pullPolicy: IfNotPresent
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: "affine-web"
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
limits:
cpu: '500m'
memory: 2Gi
requests:
cpu: '500m'
memory: 2Gi
nodeSelector: {}
tolerations: []
affinity: {}
probe:
initialDelaySeconds: 1

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "affine.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "affine.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "affine.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "affine.labels" -}}
helm.sh/chart: {{ include "affine.chart" . }}
{{ include "affine.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "affine.selectorLabels" -}}
app.kubernetes.io/name: {{ include "affine.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "affine.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "affine.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,64 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "affine.fullname" . -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "affine.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
- host: "{{ .Values.ingress.host }}"
http:
paths:
- path: /graphql
pathType: Prefix
backend:
service:
name: affine-graphql
port:
number: {{ .Values.graphql.service.port }}
- path: /api
pathType: Prefix
backend:
service:
name: affine-graphql
port:
number: {{ .Values.graphql.service.port }}
- path: /
pathType: Prefix
backend:
service:
name: affine-web
port:
number: {{ .Values.web.service.port }}
{{- end }}

17
.github/helm/affine/values.yaml vendored Normal file
View File

@@ -0,0 +1,17 @@
ingress:
enabled: false
className: ''
annotations:
kubernetes.io/ingress.class: nginx
host: affine.pro
tls: []
graphql:
service:
type: ClusterIP
port: 3000
web:
service:
type: ClusterIP
port: 8080

60
.github/helm/deployment_guide.md vendored Normal file
View File

@@ -0,0 +1,60 @@
# Cluster Deployment Guide
This document provides a step-by-step guide for developers on how to deploy services in a Kubernetes cluster. The following content assumes that the reader already has a basic understanding of Kubernetes concepts and operations.
### 1. Configure Service Mesh (Optional)
In the Kubernetes cluster, we optionally use Service Mesh (like Istio and Anthos Service Mesh) to manage the network interactions of microservices. If Service Mesh is already deployed on your cluster or do not need to use the service network, you can skip this step. In this step, we assume that you are using Google Kubernetes Engine (GKE) and have already installed Anthos Service Mesh on your cluster, if you wish to use another Ingress Controller, please refer to the relevant documentation.
To configure your kubectl context to interact with your Kubernetes cluster using the gcloud tool, you need to execute the following commands:
```sh
export CLUSTER_NAME=your_cluster_name
export REGION=your_cluster_region
export PROJECT=your_project_id
gcloud container clusters get-credentials $CLUSTER_NAME --region $REGION --project $PROJECT
```
In this command, you should replace `CLUSTER_NAME`, `REGION` and `PROJECT` with the actual name, region and project id of your Kubernetes cluster. This command retrieves the access credentials for your Kubernetes cluster and automatically configures kubectl to use these credentials.
Now, to inject Service Mesh for a specific Namespace, first, set the environment variable `NAMESPACE` that should correspond to your target Kubernetes Namespace. In this example, we use `prod` as the target Namespace:
```sh
export NAMESPACE=prod
```
Then, we label the Namespace which will enable Istio to automatically inject the sidecar container for all new Pods under this Namespace:
```sh
kubectl label namespace $NAMESPACE istio-injection- istio.io/rev=asm-managed --overwrite
```
Finally, we trigger the Kubernetes Deployment restart mechanism to allow existing Pods to also obtain sidecar container injection:
```sh
kubectl rollout restart deployment -n $NAMESPACE
```
### 2. Deploying the Application
Next, we will deploy our application in the Kubernetes cluster through Helm. First, set relevant environment variables:
```sh
export NAMESPACE=prod
export RELEASE=affine-cloud-prod
export PATH=.github/helm/affine-cloud
```
- `NAMESPACE` should be consistent with the first step, indicating your target Kubernetes Namespace.
- `RELEASE` is the name of your Helm release.
- `PATH` is the location of your Helm chart in your file system.
Finally, use the `helm upgrade --install` command to deploy or upgrade your application:
```sh
helm upgrade --namespace $NAMESPACE --create-namespace --install $RELEASE $PATH
```
This command creates (if it doesn't already exist) and deploys your Helm chart in the specified Namespace. If the release already exists, it will be upgraded.
The above are the complete steps for deploying an application in a Kubernetes cluster. Make sure all prerequisites are met before deploying, and also ensure that you have the correct permissions for operations in Kubernetes.

View File

@@ -13,6 +13,7 @@ on:
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
pull_request:
merge_group:
branches:
- master
- v[0-9]+.[0-9]+.x-staging
@@ -26,6 +27,7 @@ on:
env:
DEBUG: napi:*
BUILD_TYPE: canary
APP_NAME: affine
COVERAGE: true
MACOSX_DEPLOYMENT_TARGET: '10.13'
@@ -41,12 +43,22 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Run checks
run: |
yarn i18n-codegen gen
yarn typecheck
yarn lint --max-warnings=0
yarn circular
- name: Run i18n codegen
run: yarn i18n-codegen gen
- name: Run Type Check
run: yarn typecheck
- name: Run ESLint
run: yarn lint --max-warnings=0 --cache
- name: Run Prettier
run: yarn prettier . --ignore-unknown --cache --check
- name: Run circular
run: yarn circular
- name: Upload server dist
uses: actions/upload-artifact@v3
with:
name: server-dist
path: ./apps/server/dist
if-no-files-found: error
build-docs:
name: Build Docs
@@ -77,22 +89,13 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: storybook
path: ./packages/storybook/storybook-static
path: ./apps/storybook/storybook-static
if-no-files-found: error
build-web:
name: Build @affine/web
runs-on: ubuntu-latest
environment: development
env:
API_SERVER_PROFILE: local
ENABLE_DEBUG_PAGE: 1
ENABLE_PLUGIN: true
ENABLE_ALL_PAGE_FILTER: true
ENABLE_LEGACY_PROVIDER: true
ENABLE_PRELOADING: false
ENABLE_NEW_SETTING_MODAL: false
ENABLE_SQLITE_PROVIDER: false
steps:
- uses: actions/checkout@v3
@@ -111,14 +114,6 @@ jobs:
name: Build @affine/web (Desktop)
runs-on: ubuntu-latest
environment: development
env:
API_SERVER_PROFILE: affine
ENABLE_DEBUG_PAGE: 1
ENABLE_PLUGIN: true
ENABLE_ALL_PAGE_FILTER: true
ENABLE_LEGACY_PROVIDER: false
ENABLE_PRELOADING: false
ENABLE_NEW_SETTING_MODAL: false
steps:
- uses: actions/checkout@v3
@@ -174,10 +169,21 @@ jobs:
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Setup Rust
uses: ./.github/actions/setup-rust
with:
target: 'x86_64-unknown-linux-gnu'
- name: Run server tests
run: yarn nx test:coverage @affine/server
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Upload storage.node
uses: actions/upload-artifact@v3
with:
name: storage.node
path: ./packages/storage/storage.node
if-no-files-found: error
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
with:
@@ -185,7 +191,7 @@ jobs:
files: ./apps/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: true
fail_ci_if_error: false
storybook-test:
name: Storybook Test
@@ -202,9 +208,9 @@ jobs:
uses: actions/download-artifact@v3
with:
name: storybook
path: ./packages/storybook/storybook-static
path: ./apps/storybook/storybook-static
- name: Run storybook tests
working-directory: ./packages/storybook
working-directory: ./apps/storybook
run: |
yarn exec concurrently -k -s first -n "SB,TEST" -c "magenta,blue" "yarn exec serve ./storybook-static -l 6006" "yarn exec wait-on tcp:6006 && yarn test"
@@ -243,7 +249,7 @@ jobs:
uses: actions/download-artifact@v3
with:
name: storybook
path: ./packages/storybook/storybook-static
path: ./apps/storybook/storybook-static
- name: Wait for Octobase Ready
run: |
@@ -264,7 +270,7 @@ jobs:
files: ./.coverage/lcov.info
flags: e2etest
name: affine
fail_ci_if_error: true
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
@@ -278,6 +284,7 @@ jobs:
name: E2E Migration Test
runs-on: ubuntu-latest
environment: development
needs: [build-web]
steps:
- uses: actions/checkout@v3
@@ -286,6 +293,12 @@ jobs:
with:
playwright-install: true
- name: Download next static
uses: actions/download-artifact@v3
with:
name: next-js-static
path: ./apps/web/out
- name: Unzip
run: yarn unzip
working-directory: ./tests/affine-legacy/0.7.0-canary.18
@@ -294,6 +307,18 @@ jobs:
run: yarn e2e --forbid-only
working-directory: ./tests/affine-legacy/0.7.0-canary.18
- name: Run vitest
run: yarn test
working-directory: ./tests/affine-legacy/0.7.0-canary.18
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-migration
path: ./tests/affine-legacy/0.7.0-canary.18/test-results
if-no-files-found: ignore
desktop-test:
name: Desktop Test
runs-on: ${{ matrix.spec.os }}
@@ -349,8 +374,8 @@ jobs:
run: yarn nx test @affine/monorepo
env:
NATIVE_TEST: 'true'
- name: Build layers
run: yarn workspace @affine/electron build
- name: Build AFFiNE Desktop
run: yarn nx build @affine/electron
- name: Download static resource artifact
uses: actions/download-artifact@v3
with:
@@ -359,13 +384,13 @@ jobs:
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine/electron test
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn nx test @affine/electron
env:
COVERAGE: true
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
run: yarn workspace @affine/electron test
run: yarn nx test @affine/electron
env:
COVERAGE: true
@@ -381,7 +406,7 @@ jobs:
files: ./.coverage/lcov.info
flags: e2etest-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
name: affine
fail_ci_if_error: true
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
@@ -420,13 +445,15 @@ jobs:
files: ./.coverage/store/lcov.info
flags: unittest
name: affine
fail_ci_if_error: true
fail_ci_if_error: false
build-docker:
if: github.ref == 'refs/heads/master'
name: Build Docker
needs:
- build-web-desktop
- lint
- desktop-test
- server-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -435,6 +462,16 @@ jobs:
with:
name: next-js-static
path: ./apps/web/out
- name: Download server dist
uses: actions/download-artifact@v3
with:
name: server-dist
path: ./apps/server/dist
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./apps/server
- name: Setup Git short hash
run: |
echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
@@ -460,14 +497,21 @@ jobs:
file: .github/deployment/front/Dockerfile
tags: ghcr.io/toeverything/affine-front:${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-front:latest
# setup node without cache configuration
# Prisma cache is not compatible with docker build cache
- name: Setup Node.js
uses: ./.github/actions/setup-node
uses: actions/setup-node@v3
with:
package-install: false
node-version-file: '.nvmrc'
registry-url: https://npm.pkg.github.com
scope: '@toeverything'
- name: Install Node.js dependencies
run: yarn workspaces focus @affine/server --production
- name: Generate Prisma client
run: yarn workspace @affine/server prisma generate
- name: Build graphql Dockerfile
uses: docker/build-push-action@v4
with:

17
.github/workflows/cancel.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Cancel
on:
pull_request_target:
types:
- edited
- synchronize
jobs:
cancel:
name: 'Cancel Previous Runs'
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: styfle/cancel-workflow-action@0.11.0
with:
workflow_id: 44038251
access_token: ${{ github.token }}

View File

@@ -15,6 +15,7 @@ on:
push:
branches: [master]
pull_request:
merge_group:
# The branches below must be a subset of the branches above
branches: [master]

View File

@@ -3,8 +3,8 @@ name: Release Charts
on:
push:
branches: [master]
pull_request:
branches: [master]
paths:
- '.github/helm/**/Chart.yml'
jobs:
release:
@@ -39,19 +39,24 @@ jobs:
- name: Package charts
working-directory: .helm-chart-repo
run: |
set -ex
git config --local user.name "$GITHUB_ACTOR"
git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com"
mkdir -p .cr-index
owner=$(cut -d '/' -f 1 <<< '${{ github.repository }}')
repo=helm-charts
git_hash=$(git rev-parse HEAD)
echo $git_hash
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm dependencies build ../.github/helm/affine
helm dependencies build ../.github/helm/affine-cloud
cr package ../.github/helm/affine
cr package ../.github/helm/affine-cloud
- name: Publish charts
working-directory: .helm-chart-repo
run: |
set -ex
git config --local user.name "$GITHUB_ACTOR"
git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com"
owner=$(cut -d '/' -f 1 <<< '${{ github.repository }}')
repo=helm-charts
git_hash=$(git rev-parse HEAD)
cr upload --commit "$git_hash" \
--git-repo "$repo" --owner "$owner" \
--token '${{ secrets.HELM_RELEASER_TOKEN }}' \

View File

@@ -58,10 +58,6 @@ jobs:
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
API_SERVER_PROFILE: prod
ENABLE_TEST_PROPERTIES: false
ENABLE_BOOKMARK_OPERATION: true
ENABLE_SQLITE_PROVIDER: false
RELEASE_VERSION: ${{ needs.set-build-version.outputs.version }}
- name: Upload Artifact (web-static)
@@ -125,8 +121,11 @@ jobs:
name: before-make-web-static
path: apps/electron/resources/web-static
- name: Build Infra
run: yarn nx run-many -t build -p plugin-infra infra
- name: Build layers
run: yarn workspace @affine/electron build
run: yarn nx build @affine/electron
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'darwin' }}

View File

@@ -1,6 +1,9 @@
name: Release Desktop App
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-canary.[0-9]+'
workflow_dispatch:
inputs:
version:
@@ -28,14 +31,8 @@ permissions:
contents: write
security-events: write
concurrency:
# The concurrency group contains the workflow name and the branch name for
# pull requests or the commit hash for any other events.
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
cancel-in-progress: true
env:
BUILD_TYPE: ${{ github.event.inputs.build-type }}
BUILD_TYPE: ${{ github.event.inputs.build-type || (github.ref_type == 'tag' && contains(github.ref, 'canary') && 'canary') }}
DEBUG: napi:*
APP_NAME: affine
MACOSX_DEPLOYMENT_TARGET: '10.13'
@@ -43,22 +40,31 @@ env:
jobs:
before-make:
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
environment: production
outputs:
RELEASE_VERSION: ${{ steps.get-canary-version.outputs.RELEASE_VERSION }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Get canary version
id: get-canary-version
if: ${{ github.ref_type == 'tag' }}
run: |
TAG_VERSION=${GITHUB_REF#refs/tags/v}
PACKAGE_VERSION=$(node -p "require('./apps/electron/package.json').version")
if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then
echo "Tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)"
exit 1
fi
echo "RELEASE_VERSION=$(node -p "require('./apps/electron/package.json').version")" >> $GITHUB_OUTPUT
- name: generate-assets
run: yarn workspace @affine/electron generate-assets
env:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
API_SERVER_PROFILE: prod
ENABLE_TEST_PROPERTIES: false
ENABLE_BOOKMARK_OPERATION: true
ENABLE_SQLITE_PROVIDER: false
RELEASE_VERSION: ${{ github.event.inputs.version }}
RELEASE_VERSION: ${{ github.event.inputs.version || steps.get-canary-version.outputs.RELEASE_VERSION }}
- name: Upload Artifact (web-static)
uses: actions/upload-artifact@v3
@@ -67,7 +73,7 @@ jobs:
path: apps/electron/resources/web-static
make-distribution:
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
environment: production
strategy:
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
@@ -117,8 +123,11 @@ jobs:
name: before-make-web-static
path: apps/electron/resources/web-static
- name: Build layers
run: yarn workspace @affine/electron build
- name: Build Infra
run: yarn nx run-many -t build -p plugin-infra infra
- name: Build AFFiNE Desktop
run: yarn nx build @affine/electron
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'darwin' }}
@@ -158,7 +167,7 @@ jobs:
path: builds
release:
needs: make-distribution
needs: [before-make, make-distribution]
runs-on: ubuntu-latest
steps:
@@ -191,16 +200,16 @@ jobs:
cp ./apps/electron/scripts/generate-yml.js .
node generate-yml.js
env:
RELEASE_VERSION: ${{ github.event.inputs.version }}
RELEASE_VERSION: ${{ github.event.inputs.version || needs.before-make.outputs.RELEASE_VERSION }}
- name: Create Release Draft
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
name: Desktop APP ${{ github.event.inputs.version }}
body: 'TODO: Add release notes here'
draft: ${{ github.event.inputs.is-draft }}
prerelease: ${{ github.event.inputs.is-pre-release }}
name: ${{ github.event.inputs.version || needs.before-make.outputs.RELEASE_VERSION }}
body: ''
draft: ${{ github.event.inputs.is-draft || true }}
prerelease: ${{ github.event.inputs.is-pre-release || needs.before-make.outputs.version }}
files: |
./VERSION
./*.zip

View File

@@ -2,7 +2,10 @@
. "$(dirname -- "$0")/_/husky.sh"
# check lockfile is up to date
yarn install
yarn install --mode=update-lockfile
# lint staged files
yarn exec lint-staged
# type check
yarn typecheck

View File

@@ -1,5 +1,12 @@
pnpm-lock.yaml
yarn.lock
target
lib
test-results
packages/i18n/src/i18n-generated.ts
packages/graphql/src/graphql/index.ts
.next
out
dist
.yarn
tests/affine-legacy/0.7.0-canary.18/static
.github/helm

View File

@@ -197,7 +197,7 @@ See [LICENSE] for details.
[rust-version-icon]: https://img.shields.io/badge/Rust-1.70.0-dea584
[stars-icon]: https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars
[codecov]: https://codecov.io/gh/toeverything/affine/branch/master/graphs/badge.svg?branch=master
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.16.0-success
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.16.1-success
[typescript-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/affine/dev/typescript
[react-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/affine/dev/react?color=rgb%2897%2C%20218%2C%20251%29
[blocksuite-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/AFFiNE/@blocksuite/store?color=6880ff&filename=apps%2Fweb%2Fpackage.json&label=blocksuite

25
apps/README.md Normal file
View File

@@ -0,0 +1,25 @@
# Apps structure
> This is the structure of the `apps` directory.
## docs
AFFiNE Developer Documentation using [waku](https://github.com/dai-shi/waku).
## electron
> `web` needs to be built before electron.
AFFiNE Desktop (macOS, Linux and Windows Distribution) using [Electron](https://www.electronjs.org/).
## server
Server using [Nest.js](https://nestjs.com/).
## storybook
Storybook using [Storybook](https://storybook.js.org/).
## web
AFFiNE Core Application using [React.js](https://reactjs.org/).

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/docs",
"version": "0.7.0-canary.20",
"version": "0.7.0-canary.25",
"type": "module",
"private": true,
"scripts": {
@@ -10,12 +10,12 @@
},
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/blocks": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/editor": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/global": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/lit": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/store": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/block-std": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/blocks": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/editor": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/global": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/lit": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/store": "0.0.0-20230630081054-55a25248-nightly",
"express": "^4.18.2",
"jotai": "^2.2.1",
"react": "18.3.0-canary-8ec962d82-20230623",
@@ -30,6 +30,6 @@
"@vanilla-extract/vite-plugin": "^3.8.2",
"autoprefixer": "^10.4.14",
"tailwindcss": "^3.3.2",
"typescript": "^5.1.3"
"typescript": "^5.1.5"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.7.0-canary.20",
"version": "0.7.0-canary.25",
"author": "affine",
"repository": {
"url": "https://github.com/toeverything/AFFiNE",
@@ -11,8 +11,7 @@
"homepage": "https://github.com/toeverything/AFFiNE",
"scripts": {
"dev": "yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs",
"watch": "yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs --watch",
"prod": "yarn node scripts/dev.mjs",
"dev:prod": "yarn node scripts/dev.mjs",
"build": "zx scripts/build-layers.mjs",
"generate-assets": "zx scripts/generate-assets.mjs",
"package": "electron-forge package",
@@ -23,16 +22,14 @@
"forge": "./forge.config.js"
},
"main": "./dist/main.js",
"exports": {
"./scripts/plugins/build-plugins.mjs": "./scripts/plugins/build-plugins.mjs"
},
"devDependencies": {
"@affine-test/kit": "workspace:*",
"@affine/bookmark-block": "workspace:*",
"@affine/native": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/editor": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/lit": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/store": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/blocks": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/editor": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/lit": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/store": "0.0.0-20230630081054-55a25248-nightly",
"@electron-forge/cli": "^6.2.1",
"@electron-forge/core": "^6.2.1",
"@electron-forge/core-utils": "^6.2.1",
@@ -50,7 +47,7 @@
"electron-log": "^5.0.0-beta.24",
"electron-squirrel-startup": "1.0.0",
"electron-window-state": "^5.0.3",
"esbuild": "^0.18.9",
"esbuild": "^0.18.10",
"fs-extra": "^11.1.1",
"jotai": "^2.2.1",
"playwright": "=1.33.0",

View File

@@ -1,12 +1,9 @@
#!/usr/bin/env zx
import 'zx/globals';
import { resolve } from 'node:path';
import { spawnSync } from 'child_process';
import * as esbuild from 'esbuild';
import { config, rootDir } from './common.mjs';
import { config } from './common.mjs';
const NODE_ENV =
process.env.NODE_ENV === 'development' ? 'development' : 'production';
@@ -18,20 +15,10 @@ if (process.platform === 'win32') {
async function buildLayers() {
const common = config();
console.log('Build plugin infra');
spawnSync('yarn', ['build'], {
stdio: 'inherit',
cwd: resolve(rootDir, './packages/plugin-infra'),
});
console.log('Build plugins');
await import('./plugins/build-plugins.mjs');
await esbuild.build(common.workers);
await esbuild.build({
...common.layers,
define: {
...common.define,
'process.env.NODE_ENV': `"${NODE_ENV}"`,
'process.env.BUILD_TYPE': `"${process.env.BUILD_TYPE || 'stable'}"`,
},

View File

@@ -15,16 +15,9 @@ const DEV_SERVER_URL = process.env.DEV_SERVER_URL;
/** @type 'production' | 'development'' */
const mode = (process.env.NODE_ENV = process.env.NODE_ENV || 'development');
// List of env that will be replaced by esbuild
const ENV_MACROS = ['AFFINE_GOOGLE_CLIENT_ID', 'AFFINE_GOOGLE_CLIENT_SECRET'];
/** @return {{layers: import('esbuild').BuildOptions, workers: import('esbuild').BuildOptions}} */
export const config = () => {
const define = Object.fromEntries([
...ENV_MACROS.map(key => [
'process.env.' + key,
JSON.stringify(process.env[key] ?? ''),
]),
['process.env.NODE_ENV', `"${mode}"`],
['process.env.USE_WORKER', '"true"'],
]);

View File

@@ -1,13 +1,12 @@
/* eslint-disable no-async-promise-executor */
import { spawn } from 'node:child_process';
import { readFileSync } from 'node:fs';
import path, { resolve } from 'node:path';
import path from 'node:path';
import electronPath from 'electron';
import * as esbuild from 'esbuild';
import which from 'which';
import { config, electronDir, rootDir } from './common.mjs';
import { config, electronDir } from './common.mjs';
// this means we don't spawn electron windows, mainly for testing
const watchMode = process.argv.includes('--watch');
@@ -68,14 +67,6 @@ function spawnOrReloadElectron() {
}
const common = config();
const yarnPath = which.sync('yarn');
async function watchPlugins() {
spawn(yarnPath, ['dev'], {
stdio: 'inherit',
cwd: resolve(rootDir, './packages/plugin-infra'),
});
await import('./plugins/dev-plugins.mjs');
}
async function watchLayers() {
return new Promise(async resolve => {
@@ -134,7 +125,6 @@ async function watchWorkers() {
}
async function main() {
await watchPlugins();
await watchLayers();
await watchWorkers();

View File

@@ -0,0 +1,9 @@
import { clipboard, nativeImage } from 'electron';
import type { NamespaceHandlers } from '../type';
export const clipboardHandlers = {
copyAsPng: async (_, dataURL: string) => {
clipboard.writeImage(nativeImage.createFromDataURL(dataURL));
},
} satisfies NamespaceHandlers;

View File

@@ -1,4 +1,4 @@
import { BrowserWindow, dialog, shell } from 'electron';
import { BrowserWindow, dialog } from 'electron';
import fs from 'fs-extra';
import { logger } from '../logger';
@@ -49,8 +49,6 @@ export async function savePDFFileAs(
logger.log(`Wrote PDF successfully to ${filePath}`);
});
});
await shell.openPath(filePath);
return { filePath };
} catch (err) {
logger.error('savePDFFileAs', err);

View File

@@ -1,4 +1,5 @@
import type {
ClipboardHandlerManager,
DebugHandlerManager,
ExportHandlerManager,
UIHandlerManager,
@@ -7,6 +8,7 @@ import type {
} from '@toeverything/infra';
import { ipcMain } from 'electron';
import { clipboardHandlers } from './clipboard';
import { exportHandlers } from './export';
import { getLogFilePath, logger, revealLogFile } from './logger';
import { uiHandlers } from './ui';
@@ -26,6 +28,10 @@ type AllHandlers = {
Electron.IpcMainInvokeEvent,
DebugHandlerManager
>;
clipboard: UnwrapManagerHandlerToServerSide<
Electron.IpcMainInvokeEvent,
ClipboardHandlerManager
>;
export: UnwrapManagerHandlerToServerSide<
Electron.IpcMainInvokeEvent,
ExportHandlerManager
@@ -44,6 +50,7 @@ type AllHandlers = {
export const allHandlers = {
debug: debugHandlers,
ui: uiHandlers,
clipboard: clipboardHandlers,
export: exportHandlers,
updater: updaterHandlers,
} satisfies AllHandlers;

View File

@@ -7,6 +7,7 @@ import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('appInfo', appInfo);
contextBridge.exposeInMainWorld('apis', apis);
contextBridge.exposeInMainWorld('events', events);
contextBridge.exposeInMainWorld('platform', process.platform);
// Credit to microsoft/vscode
const globals = {

View File

@@ -140,3 +140,12 @@ test('affine onboarding button', async ({ page }) => {
expect(await onboardingModal.isVisible()).toEqual(false);
});
test('windows only check', async ({ page }) => {
const windowOnlyUI = page.locator('[data-platform-target=win32]');
if (process.platform === 'win32') {
await expect(windowOnlyUI).toBeVisible();
} else {
await expect(windowOnlyUI).not.toBeVisible();
}
});

View File

@@ -9,5 +9,5 @@
"allowSyntheticDefaultImports": true,
"noEmit": false
},
"include": ["./scripts"]
"include": ["./scripts", "esbuild.main.config.ts", "esbuild.plugin.config.ts"]
}

View File

@@ -1,3 +1,23 @@
# Server
The latest server code of AFFiNE is at https://github.com/toeverything/OctoBase/tree/master/apps/cloud
## Get started
### Install dependencies
```bash
yarn
```
### Build Native binding
```bash
yarn workspace @affine/storage build
```
### Run server
```bash
yarn dev
```
now you can access the server GraphQL endpoint at http://localhost:3000/graphql

View File

@@ -0,0 +1,52 @@
-- CreateTable
CREATE TABLE "blobs" (
"hash" VARCHAR NOT NULL,
"workspace_id" VARCHAR NOT NULL,
"blob" BYTEA NOT NULL,
"length" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "blobs_pkey" PRIMARY KEY ("hash")
);
-- CreateTable
CREATE TABLE "optimized_blobs" (
"hash" VARCHAR NOT NULL,
"workspace_id" VARCHAR NOT NULL,
"params" VARCHAR NOT NULL,
"blob" BYTEA NOT NULL,
"length" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "optimized_blobs_pkey" PRIMARY KEY ("hash")
);
-- CreateTable
CREATE TABLE "docs" (
"id" SERIAL NOT NULL,
"workspace_id" VARCHAR NOT NULL,
"guid" VARCHAR NOT NULL,
"is_workspace" BOOLEAN NOT NULL DEFAULT true,
"blob" BYTEA NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "docs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "blobs_workspace_id_hash_key" ON "blobs"("workspace_id", "hash");
-- CreateIndex
CREATE UNIQUE INDEX "optimized_blobs_workspace_id_hash_params_key" ON "optimized_blobs"("workspace_id", "hash", "params");
-- CreateIndex
CREATE INDEX "docs_workspace_id_guid_idx" ON "docs"("workspace_id", "guid");
-- AddForeignKey
ALTER TABLE "blobs" ADD CONSTRAINT "blobs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "optimized_blobs" ADD CONSTRAINT "optimized_blobs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "docs" ADD CONSTRAINT "docs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/server",
"private": true,
"version": "0.7.0-canary.20",
"version": "0.7.0-canary.25",
"description": "Affine Node.js server",
"type": "module",
"bin": {
@@ -10,6 +10,7 @@
"scripts": {
"dev": "nodemon ./src/index.ts",
"test": "yarn exec ts-node-esm ./scripts/run-test.ts all",
"test:watch": "yarn exec ts-node-esm ./scripts/run-test.ts all --watch",
"test:coverage": "c8 yarn exec ts-node-esm ./scripts/run-test.ts all",
"postinstall": "prisma generate"
},
@@ -39,6 +40,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
"@affine/storage": "workspace:*",
"@napi-rs/image": "^1.6.1",
"@nestjs/testing": "^10.0.3",
"@types/express": "^4.17.17",
@@ -49,7 +51,7 @@
"nodemon": "^2.0.22",
"supertest": "^6.3.3",
"ts-node": "^10.9.1",
"typescript": "^5.1.3"
"typescript": "^5.1.5"
},
"nodemonConfig": {
"exec": "node",

View File

@@ -1,5 +1,6 @@
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-3.0.x"]
}
datasource db {
@@ -8,10 +9,13 @@ datasource db {
}
model Workspace {
id String @id @default(uuid()) @db.VarChar
public Boolean
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
users UserWorkspacePermission[]
id String @id @default(uuid()) @db.VarChar
public Boolean
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
users UserWorkspacePermission[]
blobs Blob[]
docs Doc[]
optimizedBlobs OptimizedBlob[]
@@map("workspaces")
}
@@ -86,3 +90,44 @@ model VerificationToken {
@@unique([identifier, token])
@@map("verificationtokens")
}
model Blob {
hash String @id @default(uuid()) @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
blob Bytes @db.ByteA
length Int
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@unique([workspaceId, hash])
@@map("blobs")
}
model OptimizedBlob {
hash String @id @default(uuid()) @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
params String @db.VarChar
blob Bytes @db.ByteA
length Int
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@unique([workspaceId, hash, params])
@@map("optimized_blobs")
}
model Doc {
id Int @id @default(autoincrement()) @db.Integer
workspaceId String @map("workspace_id") @db.VarChar
guid String @db.VarChar
is_workspace Boolean @default(true) @db.Boolean
blob Bytes @db.ByteA
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@index([workspaceId, guid])
@@map("docs")
}

View File

@@ -12,7 +12,13 @@ const root = fileURLToPath(new URL('..', import.meta.url));
const testDir = resolve(root, 'src', 'tests');
const files = await readdir(testDir);
const sharedArgs = [...pkg.nodemonConfig.nodeArgs, '--test'];
const watchMode = process.argv.includes('--watch');
const sharedArgs = [
...pkg.nodemonConfig.nodeArgs,
'--test',
watchMode ? '--watch' : '',
];
const env = {
PATH: process.env.PATH,

View File

@@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common';
import pkg from '../package.json' assert { type: 'json' };
@Controller('/')
export class AppController {
@Get()
hello() {
return {
message: `AFFiNE GraphQL server: ${pkg.version}`,
};
}
}

View File

@@ -1,17 +1,20 @@
/// <reference types="./global.d.ts" />
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ConfigModule } from './config';
import { GqlModule } from './graphql.module';
import { BusinessModules } from './modules';
import { PrismaModule } from './prisma';
import { StorageModule } from './storage';
@Module({
imports: [
PrismaModule,
GqlModule,
ConfigModule.forRoot(),
StorageModule.forRoot(),
...BusinessModules,
],
controllers: [AppController],
})
export class AppModule {}

View File

@@ -106,7 +106,7 @@ export interface AFFiNEConfig {
/**
* which port the server will listen on
*
* @default 3000
* @default 3010
* @env AFFINE_SERVER_PORT
*/
port: number;
@@ -132,6 +132,13 @@ export interface AFFiNEConfig {
*/
get origin(): string;
/**
* the database config
*/
db: {
url: string;
};
/**
* the apollo driver config
*/
@@ -146,23 +153,13 @@ export interface AFFiNEConfig {
/**
* whether use remote object storage
*/
enable: boolean;
/**
* used to store all uploaded builds and analysis reports
*
* the concrete type definition is not given here because different storage providers introduce
* significant differences in configuration
*
* @example
* {
* provider: 'aws',
* region: 'eu-west-1',
* aws_access_key_id: '',
* aws_secret_access_key: '',
* // other aws storage config...
* }
*/
config: Record<string, string>;
r2: {
enabled: boolean;
accountId: string;
bucket: string;
accessKeyId: string;
secretAccessKey: string;
};
/**
* Only used when `enable` is `false`
*/
@@ -217,6 +214,7 @@ export interface AFFiNEConfig {
Record<
ExternalAccount,
{
enabled: boolean;
clientId: string;
clientSecret: string;
/**

View File

@@ -1,5 +1,6 @@
/// <reference types="../global.d.ts" />
import { createPrivateKey, createPublicKey } from 'node:crypto';
import { homedir } from 'node:os';
import { join } from 'node:path';
@@ -7,77 +8,130 @@ import parse from 'parse-duration';
import pkg from '../../package.json' assert { type: 'json' };
import type { AFFiNEConfig } from './def';
import { applyEnvToConfig } from './env';
// Don't use this in production
export const examplePublicKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnxM+GhB6eNKPmTP6uH5Gpr+bmQ87
hHGeOiCsay0w/aPwMqzAOKkZGqX+HZ9BNGy/yiXmnscey5b2vOTzxtRvxA==
-----END PUBLIC KEY-----`;
export const examplePrivateKey = `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIEtyAJLIULkphVhqXqxk4Nr8Ggty3XLwUJWBxzAWCWTMoAoGCCqGSM49
AwEHoUQDQgAEF3U/0wIeJ3jRKXeFKqQyBKlr9F7xaAUScRrAuSP33rajm3cdfihI
3JvMxVNsS2lE8PSGQrvDrJZaDo0L+Lq9Gg==
-----END EC PRIVATE KEY-----`;
// Don't use this in production
export const examplePrivateKey = `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWOog5SFXs1Vjh/WP
QCYPQKgf/jsNmWsvD+jYSn6mi3yhRANCAASfEz4aEHp40o+ZM/q4fkamv5uZDzuE
cZ46IKxrLTD9o/AyrMA4qRkapf4dn0E0bL/KJeaexx7Llva85PPG1G/E
-----END PRIVATE KEY-----`;
const jwtKeyPair = (function () {
const AUTH_PRIVATE_KEY = process.env.AUTH_PRIVATE_KEY ?? examplePrivateKey;
const privateKey = createPrivateKey({
key: Buffer.from(AUTH_PRIVATE_KEY),
format: 'pem',
type: 'sec1',
})
.export({
format: 'pem',
type: 'pkcs8',
})
.toString('utf8');
const publicKey = createPublicKey({
key: Buffer.from(AUTH_PRIVATE_KEY),
format: 'pem',
type: 'spki',
})
.export({
format: 'pem',
type: 'spki',
})
.toString('utf8');
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
serverId: 'affine-nestjs-server',
version: pkg.version,
ENV_MAP: {},
env: process.env.NODE_ENV ?? 'development',
get prod() {
return this.env === 'production';
},
get dev() {
return this.env === 'development';
},
get test() {
return this.env === 'test';
},
get deploy() {
return !this.dev && !this.test;
},
https: false,
host: 'localhost',
port: 3010,
path: '',
get origin() {
return this.dev
? 'http://localhost:8080'
: `${this.https ? 'https' : 'http'}://${this.host}${
this.host === 'localhost' ? `:${this.port}` : ''
}`;
},
get baseUrl() {
return `${this.origin}${this.path}`;
},
graphql: {
buildSchemaOptions: {
numberScalarMode: 'integer',
return {
publicKey,
privateKey,
};
})();
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
const defaultConfig = {
serverId: 'affine-nestjs-server',
version: pkg.version,
ENV_MAP: {
AFFINE_SERVER_PORT: 'port',
AFFINE_SERVER_HOST: 'host',
AFFINE_SERVER_SUB_PATH: 'path',
DATABASE_URL: 'db.url',
AUTH_PRIVATE_KEY: 'auth.privateKey',
ENABLE_R2_OBJECT_STORAGE: 'objectStorage.r2.enabled',
R2_OBJECT_STORAGE_ACCOUNT_ID: 'objectStorage.r2.accountId',
R2_OBJECT_STORAGE_ACCESS_KEY_ID: 'objectStorage.r2.accessKeyId',
R2_OBJECT_STORAGE_SECRET_ACCESS_KEY: 'objectStorage.r2.secretAccessKey',
R2_OBJECT_STORAGE_BUCKET: 'objectStorage.r2.bucket',
OAUTH_GOOGLE_CLIENT_ID: 'auth.oauthProviders.google.clientId',
OAUTH_GOOGLE_CLIENT_SECRET: 'auth.oauthProviders.google.clientSecret',
OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId',
OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret',
} satisfies AFFiNEConfig['ENV_MAP'],
env: process.env.NODE_ENV ?? 'development',
get prod() {
return this.env === 'production';
},
introspection: true,
playground: true,
debug: true,
},
auth: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
accessTokenExpiresIn: parse('1h')! / 1000,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
refreshTokenExpiresIn: parse('7d')! / 1000,
leeway: 60,
publicKey: examplePublicKey,
privateKey: examplePrivateKey,
enableSignup: true,
enableOauth: false,
nextAuthSecret: '',
oauthProviders: {},
},
objectStorage: {
enable: false,
config: {},
fs: {
path: join(homedir(), '.affine-storage'),
get dev() {
return this.env === 'development';
},
},
});
get test() {
return this.env === 'test';
},
get deploy() {
return !this.dev && !this.test;
},
https: false,
host: 'localhost',
port: 3010,
path: '',
db: {
url: '',
},
get origin() {
return this.dev
? 'http://localhost:8080'
: `${this.https ? 'https' : 'http'}://${this.host}${
this.host === 'localhost' ? `:${this.port}` : ''
}`;
},
get baseUrl() {
return `${this.origin}${this.path}`;
},
graphql: {
buildSchemaOptions: {
numberScalarMode: 'integer',
},
introspection: true,
playground: true,
debug: true,
},
auth: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
accessTokenExpiresIn: parse('1h')! / 1000,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
refreshTokenExpiresIn: parse('7d')! / 1000,
leeway: 60,
privateKey: jwtKeyPair.privateKey,
publicKey: jwtKeyPair.publicKey,
enableSignup: true,
enableOauth: false,
nextAuthSecret: '',
oauthProviders: {},
},
objectStorage: {
r2: {
enabled: false,
bucket: '',
accountId: '',
accessKeyId: '',
secretAccessKey: '',
},
fs: {
path: join(homedir(), '.affine-storage'),
},
},
} as const;
applyEnvToConfig(defaultConfig);
return defaultConfig;
};

View File

@@ -1,15 +1,17 @@
import { set } from 'lodash-es';
import { parseEnvValue } from './def';
import { type AFFiNEConfig, parseEnvValue } from './def';
for (const env in AFFiNE.ENV_MAP) {
const config = AFFiNE.ENV_MAP[env];
const [path, value] =
typeof config === 'string'
? [config, process.env[env]]
: [config[0], parseEnvValue(process.env[env], config[1])];
export function applyEnvToConfig(rawConfig: AFFiNEConfig) {
for (const env in rawConfig.ENV_MAP) {
const config = rawConfig.ENV_MAP[env];
const [path, value] =
typeof config === 'string'
? [config, process.env[env]]
: [config[0], parseEnvValue(process.env[env], config[1])];
if (typeof value !== 'undefined') {
set(globalThis.AFFiNE, path, process.env[env]);
if (typeof value !== 'undefined') {
set(rawConfig, path, value);
}
}
}

View File

@@ -1,9 +1,12 @@
// eslint-disable-next-line simple-import-sort/imports
import type { DynamicModule, FactoryProvider } from '@nestjs/common';
import { merge } from 'lodash-es';
import type { DeepPartial } from '../utils/types';
import type { AFFiNEConfig } from './def';
import '../prelude';
type ConstructorOf<T> = {
new (): T;
};
@@ -37,11 +40,14 @@ function createConfigProvider(
provide: Config,
useFactory: () => {
const wrapper = new Config();
const config = merge({}, AFFiNE, override);
const config = merge({}, globalThis.AFFiNE, override);
const proxy: Config = new Proxy(wrapper, {
get: (_target, property: keyof Config) => {
const desc = Object.getOwnPropertyDescriptor(AFFiNE, property);
const desc = Object.getOwnPropertyDescriptor(
globalThis.AFFiNE,
property
);
if (desc?.get) {
return desc.get.call(proxy);
}

View File

@@ -1,5 +1,4 @@
import './prelude';
/// <reference types="./global.d.ts" />
import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import { static as staticMiddleware } from 'express';
@@ -28,12 +27,12 @@ app.use(
})
);
const host = process.env.HOST ?? 'localhost';
const port = process.env.PORT ?? 3010;
const config = app.get(Config);
if (!config.objectStorage.enable) {
const host = config.host ?? 'localhost';
const port = config.port ?? 3010;
if (!config.objectStorage.r2.enabled) {
app.use('/assets', staticMiddleware(config.objectStorage.fs.path));
}

View File

@@ -62,16 +62,4 @@ export class AuthResolver {
ctx.req.user = user;
return user;
}
@Mutation(() => UserType)
async signUp(
@Context() ctx: { req: Request },
@Args('email') email: string,
@Args('password') password: string,
@Args('name') name: string
) {
const user = await this.auth.register(name, email, password);
ctx.req.user = user;
return user;
}
}

View File

@@ -8,7 +8,14 @@ export const S3_SERVICE = Symbol('S3_SERVICE');
export const S3: FactoryProvider<S3Client> = {
provide: S3_SERVICE,
useFactory: (config: Config) => {
const s3 = new S3Client(config.objectStorage.config);
const s3 = new S3Client({
region: 'auto',
endpoint: `https://${config.objectStorage.r2.accountId}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: config.objectStorage.r2.accessKeyId,
secretAccessKey: config.objectStorage.r2.secretAccessKey,
},
});
return s3;
},
inject: [Config],

View File

@@ -15,11 +15,11 @@ export class StorageService {
) {}
async uploadFile(key: string, file: FileUpload) {
if (this.config.objectStorage.enable) {
if (this.config.objectStorage.r2.enabled) {
await this.s3.send(
new PutObjectCommand({
Body: file.createReadStream(),
Bucket: this.config.objectStorage.config.bucket,
Bucket: this.config.objectStorage.r2.bucket,
Key: key,
})
);

View File

@@ -0,0 +1,36 @@
import type { Storage } from '@affine/storage';
import {
Controller,
Get,
Inject,
NotFoundException,
Param,
Res,
} from '@nestjs/common';
import type { Response } from 'express';
import { StorageProvide } from '../../storage';
@Controller('/api/workspaces')
export class WorkspacesController {
constructor(@Inject(StorageProvide) private readonly storage: Storage) {}
@Get('/:id/blobs/:name')
async blob(
@Param('id') workspaceId: string,
@Param('name') name: string,
@Res() res: Response
) {
const blob = await this.storage.blob(workspaceId, name);
if (!blob) {
throw new NotFoundException('Blob not found');
}
res.setHeader('content-type', blob.contentType);
res.setHeader('last-modified', blob.lastModified);
res.setHeader('content-length', blob.size);
res.send(blob.data);
}
}

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { WorkspacesController } from './controller';
import { PermissionService } from './permission';
import { WorkspaceResolver } from './resolver';
@Module({
providers: [WorkspaceResolver, PermissionService],
providers: [WorkspaceResolver, PermissionService, WorkspacesController],
exports: [PermissionService],
})
export class WorkspaceModule {}

View File

@@ -1,4 +1,5 @@
import { ForbiddenException, NotFoundException } from '@nestjs/common';
import type { Storage } from '@affine/storage';
import { ForbiddenException, Inject, NotFoundException } from '@nestjs/common';
import {
Args,
Field,
@@ -16,8 +17,12 @@ import {
Resolver,
} from '@nestjs/graphql';
import type { User, Workspace } from '@prisma/client';
// @ts-expect-error graphql-upload is not typed
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { PrismaService } from '../../prisma';
import { StorageProvide } from '../../storage';
import type { FileUpload } from '../../types';
import { Auth, CurrentUser } from '../auth';
import { UserType } from '../users/resolver';
import { PermissionService } from './permission';
@@ -55,7 +60,8 @@ export class UpdateWorkspaceInput extends PickType(
export class WorkspaceResolver {
constructor(
private readonly prisma: PrismaService,
private readonly permissionProvider: PermissionService
private readonly permissionProvider: PermissionService,
@Inject(StorageProvide) private readonly storage: Storage
) {}
@ResolveField(() => Permission, {
@@ -174,8 +180,25 @@ export class WorkspaceResolver {
@Mutation(() => WorkspaceType, {
description: 'Create a new workspace',
})
async createWorkspace(@CurrentUser() user: User) {
return this.prisma.workspace.create({
async createWorkspace(
@CurrentUser() user: User,
@Args({ name: 'init', type: () => GraphQLUpload })
update: FileUpload
) {
// convert stream to buffer
const buffer = await new Promise<Buffer>((resolve, reject) => {
const stream = update.createReadStream();
const chunks: Uint8Array[] = [];
stream.on('data', chunk => {
chunks.push(chunk);
});
stream.on('error', reject);
stream.on('end', () => {
resolve(Buffer.concat(chunks));
});
});
const workspace = await this.prisma.workspace.create({
data: {
public: false,
users: {
@@ -191,6 +214,10 @@ export class WorkspaceResolver {
},
},
});
await this.storage.createWorkspace(workspace.id, buffer);
return workspace;
}
@Mutation(() => WorkspaceType, {
@@ -221,8 +248,15 @@ export class WorkspaceResolver {
},
});
await this.prisma.userWorkspacePermission.deleteMany({
where: {
workspaceId: id,
},
});
// TODO:
// delete all related data, like websocket connections, blobs, etc.
await this.storage.deleteWorkspace(id);
return true;
}
@@ -283,4 +317,28 @@ export class WorkspaceResolver {
return this.permissionProvider.revoke(workspaceId, user.id);
}
@Mutation(() => String)
async uploadBlob(
@CurrentUser() user: User,
@Args('workspaceId') workspaceId: string,
@Args({ name: 'blob', type: () => GraphQLUpload })
blob: FileUpload
) {
await this.permissionProvider.check(workspaceId, user.id);
const buffer = await new Promise<Buffer>((resolve, reject) => {
const stream = blob.createReadStream();
const chunks: Uint8Array[] = [];
stream.on('data', chunk => {
chunks.push(chunk);
});
stream.on('error', reject);
stream.on('end', () => {
resolve(Buffer.concat(chunks));
});
});
return this.storage.uploadBlob(workspaceId, buffer);
}
}

View File

@@ -106,7 +106,7 @@ type Mutation {
"""
Create a new workspace
"""
createWorkspace: WorkspaceType!
createWorkspace(init: Upload!): WorkspaceType!
"""
Update workspace
@@ -121,6 +121,7 @@ type Mutation {
revoke(workspaceId: String!, userId: String!): Boolean!
acceptInvite(workspaceId: String!): Boolean!
leaveWorkspace(workspaceId: String!): Boolean!
uploadBlob(workspaceId: String!, blob: Upload!): String!
"""
Upload user avatar
@@ -128,6 +129,11 @@ type Mutation {
uploadAvatar(id: String!, avatar: Upload!): UserType!
}
"""
The `Upload` scalar type represents a file upload.
"""
scalar Upload
input UpdateWorkspaceInput {
"""
is Public workspace
@@ -135,8 +141,3 @@ input UpdateWorkspaceInput {
public: Boolean
id: ID!
}
"""
The `Upload` scalar type represents a file upload.
"""
scalar Upload

View File

@@ -0,0 +1,37 @@
import { createRequire } from 'node:module';
import type { Storage } from '@affine/storage';
import { type DynamicModule, type FactoryProvider } from '@nestjs/common';
import { Config } from '../config';
export const StorageProvide = Symbol('Storage');
const require = createRequire(import.meta.url);
export class StorageModule {
static forRoot(): DynamicModule {
const storageProvider: FactoryProvider = {
provide: StorageProvide,
useFactory: async (config: Config) => {
let StorageFactory: typeof Storage;
try {
// dev mode
StorageFactory = (await import('@affine/storage')).Storage;
} catch {
// In docker
StorageFactory = require('../../storage.node').Storage;
}
return StorageFactory.connect(config.db.url);
},
inject: [Config],
};
return {
global: true,
module: StorageModule,
providers: [storageProvider],
exports: [storageProvider],
};
}
}

View File

@@ -12,12 +12,9 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import request from 'supertest';
import { AppModule } from '../app';
import { getDefaultAFFiNEConfig } from '../config/default';
const gql = '/graphql';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
describe('AppModule', () => {
let app: INestApplication;
@@ -76,33 +73,14 @@ describe('AppModule', () => {
.auth(token, { type: 'bearer' })
.send({
query: `
mutation {
createWorkspace {
id
public
createdAt
}
query {
__typename
}
`,
})
.expect(200)
.expect(res => {
ok(
typeof res.body.data.createWorkspace === 'object',
'res.body.data.createWorkspace is not an object'
);
ok(
typeof res.body.data.createWorkspace.id === 'string',
'res.body.data.createWorkspace.id is not a string'
);
ok(
typeof res.body.data.createWorkspace.public === 'boolean',
'res.body.data.createWorkspace.public is not a boolean'
);
ok(
typeof res.body.data.createWorkspace.createdAt === 'string',
'res.body.data.createWorkspace.created_at is not a string'
);
ok(res.body.data.__typename === 'Query');
});
});

View File

@@ -1,3 +1,4 @@
/// <reference types="../global.d.ts" />
import { ok } from 'node:assert';
import { beforeEach, test } from 'node:test';
@@ -5,14 +6,11 @@ import { Test } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import { ConfigModule } from '../config';
import { getDefaultAFFiNEConfig } from '../config/default';
import { GqlModule } from '../graphql.module';
import { AuthModule } from '../modules/auth';
import { AuthService } from '../modules/auth/service';
import { PrismaModule } from '../prisma';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
let auth: AuthService;
// cleanup database before each test

View File

@@ -4,9 +4,6 @@ import { beforeEach, test } from 'node:test';
import { Test } from '@nestjs/testing';
import { Config, ConfigModule } from '../config';
import { getDefaultAFFiNEConfig } from '../config/default';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
let config: Config;
beforeEach(async () => {

View File

@@ -1,22 +1,21 @@
import { ok } from 'node:assert';
import { afterEach, beforeEach, describe, test } from 'node:test';
import { afterEach, beforeEach, describe, it } from 'node:test';
import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
// @ts-expect-error graphql-upload is not typed
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import request from 'supertest';
import { AppModule } from '../app';
import { getDefaultAFFiNEConfig } from '../config/default';
import type { TokenType } from '../modules/auth';
import type { UserType } from '../modules/users';
import type { WorkspaceType } from '../modules/workspaces';
const gql = '/graphql';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
describe('AppModule', () => {
describe('Workspace Module', () => {
let app: INestApplication;
// cleanup database before each test
@@ -32,6 +31,12 @@ describe('AppModule', () => {
imports: [AppModule],
}).compile();
app = module.createNestApplication();
app.use(
graphqlUploadExpress({
maxFileSize: 10 * 1024 * 1024,
maxFiles: 5,
})
);
await app.init();
});
@@ -63,15 +68,20 @@ describe('AppModule', () => {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.send({
query: `
mutation {
createWorkspace {
.field(
'operations',
JSON.stringify({
name: 'createWorkspace',
query: `mutation createWorkspace($init: Upload!) {
createWorkspace(init: $init) {
id
}
}
`,
})
}`,
variables: { init: null },
})
)
.field('map', JSON.stringify({ '0': ['variables.init'] }))
.attach('0', Buffer.from([0, 0]), 'init.data')
.expect(200);
return res.body.data.createWorkspace;
}
@@ -151,21 +161,21 @@ describe('AppModule', () => {
return res.body.data.revoke;
}
test('should register a user', async () => {
it('should register a user', async () => {
const user = await registerUser('u1', 'u1@affine.pro', '123456');
ok(typeof user.id === 'string', 'user.id is not a string');
ok(user.name === 'u1', 'user.name is not valid');
ok(user.email === 'u1@affine.pro', 'user.email is not valid');
});
test('should create a workspace', async () => {
it('should create a workspace', async () => {
const user = await registerUser('u1', 'u1@affine.pro', '1');
const workspace = await createWorkspace(user.token.token);
ok(typeof workspace.id === 'string', 'workspace.id is not a string');
});
test('should invite a user', async () => {
it('should invite a user', async () => {
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
@@ -180,7 +190,7 @@ describe('AppModule', () => {
ok(invite === true, 'failed to invite user');
});
test('should accept an invite', async () => {
it('should accept an invite', async () => {
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
@@ -191,7 +201,7 @@ describe('AppModule', () => {
ok(accept === true, 'failed to accept invite');
});
test('should leave a workspace', async () => {
it('should leave a workspace', async () => {
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
@@ -203,7 +213,7 @@ describe('AppModule', () => {
ok(leave === true, 'failed to leave workspace');
});
test('should revoke a user', async () => {
it('should revoke a user', async () => {
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
const u2 = await registerUser('u2', 'u2@affine.pro', '1');

View File

@@ -20,6 +20,9 @@
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "../../packages/storage/tsconfig.json"
}
],
"ts-node": {

View File

@@ -2,9 +2,13 @@ import '@affine/component/theme/global.css';
import '@affine/component/theme/theme.css';
import { LOCALES, createI18n } from '@affine/i18n';
import { ThemeProvider, useTheme } from 'next-themes';
import { ComponentType, useEffect } from 'react';
import { setupGlobal } from '@affine/env/global';
import type { ComponentType } from 'react';
import { useEffect } from 'react';
import { useDarkMode } from 'storybook-dark-mode';
setupGlobal();
export const parameters = {
backgrounds: { disable: true },
actions: { argTypesRegex: '^on[A-Z].*' },

View File

@@ -30,13 +30,13 @@
"wait-on": "^7.0.1"
},
"devDependencies": {
"@blocksuite/block-std": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/blocks": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/editor": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/global": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/icons": "^2.1.21",
"@blocksuite/lit": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/store": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/block-std": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/blocks": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/editor": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/global": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/icons": "^2.1.23",
"@blocksuite/lit": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/store": "0.0.0-20230630081054-55a25248-nightly",
"react": "18.3.0-canary-8ec962d82-20230623",
"react-dom": "18.3.0-canary-8ec962d82-20230623"
},
@@ -48,5 +48,5 @@
"@blocksuite/lit": "*",
"@blocksuite/store": "*"
},
"version": "0.7.0-canary.20"
"version": "0.7.0-canary.25"
}

Some files were not shown because too many files have changed in this diff Show More