1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-09 17:44:59 +09:00

GO-4779 Merge branch 'GO-4146-new-spacestore' of github.com:anyproto/anytype-heart into go-4779-show-last-message-preview-from-chat-in-vault

This commit is contained in:
Sergey 2025-02-05 10:09:38 +01:00
commit a0059dda9e
No known key found for this signature in database
GPG key ID: 3B6BEF79160221C6
217 changed files with 35458 additions and 5350 deletions

View file

@ -10,7 +10,7 @@ permissions:
contents: write
pull-requests: write
statuses: write
jobs:
cla-check:
uses: anyproto/open/.github/workflows/cla.yml@main

241
.github/workflows/nightly.yml vendored Normal file
View file

@ -0,0 +1,241 @@
name: Nightly Builds
on:
push:
branches:
- 'nightly*'
workflow_dispatch:
inputs:
channel:
description: electron.builder channel
required: true
default: alpha
type: choice
options:
- alpha
- beta
# schedule:
# - cron: '0 0 * * *' # every day at midnight
# filters:
# branches:
# include:
# - 'nightly-ci-test'
permissions:
actions: 'write'
packages: 'write'
contents: 'write'
jobs:
build:
runs-on: 'macos-14'
steps:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: 1.23.2
check-latest: true
- name: Setup GO
run: |
go version
echo GOPATH=$(go env GOPATH) >> $GITHUB_ENV
echo GOBIN=$(go env GOPATH)/bin >> $GITHUB_ENV
echo $(go env GOPATH)/bin >> $GITHUB_PATH
- name: Install brew and node deps
run: |
curl https://raw.githubusercontent.com/Homebrew/homebrew-core/31b24d65a7210ea0a5689d5ad00dd8d1bf5211db/Formula/protobuf.rb --output protobuf.rb
curl https://raw.githubusercontent.com/Homebrew/homebrew-core/d600b1f7119f6e6a4e97fb83233b313b0468b7e4/Formula/s/swift-protobuf.rb --output swift-protobuf.rb
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 brew install ./protobuf.rb
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 brew install --ignore-dependencies ./swift-protobuf.rb
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 brew install mingw-w64
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 brew install grpcurl
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 brew tap messense/macos-cross-toolchains && brew install x86_64-unknown-linux-musl && brew install aarch64-unknown-linux-musl
npm i -g node-gyp
- name: Checkout
uses: actions/checkout@v3
- name: Nightly mode env settings
shell: bash
run: |
# choice channel name {{
if [[ -z "${{ github.event.inputs.channel }}" ]]; then
# choice default value for channel from ref name
if echo "${{ github.ref_name }}" | grep -q "beta"; then
CHANNEL="beta"
else
CHANNEL="alpha"
fi
else
CHANNEL="${{github.event.inputs.channel}}"
fi
echo "CHANNEL=$CHANNEL" >> $GITHUB_ENV
# }}
# choice s3 bucket for publishing {{
if [[ "$CHANNEL" == "beta" ]]; then
S3_BUCKET="${{secrets.NIGHTLY_AWS_S3_BUCKET_BETA}}"
else
S3_BUCKET="${{secrets.NIGHTLY_AWS_S3_BUCKET}}"
fi
echo "S3_BUCKET=$S3_BUCKET" >> $GITHUB_ENV
# }}
- name: Set env vars
env:
UNSPLASH_KEY: ${{ secrets.UNSPLASH_KEY }}
INHOUSE_KEY: ${{ secrets.INHOUSE_KEY }}
run: |
GIT_SUMMARY=$(git describe --tags --always)
echo "FLAGS=-X github.com/anyproto/anytype-heart/util/vcs.GitSummary=${GIT_SUMMARY} -X github.com/anyproto/anytype-heart/metrics.DefaultInHouseKey=${INHOUSE_KEY} -X github.com/anyproto/anytype-heart/util/unsplash.DefaultToken=${UNSPLASH_KEY}" >> $GITHUB_ENV
VERSION="nightly"
echo "${{ secrets.STAGING_NODE_CONF }}" > ./core/anytype/config/nodes/custom.yml
echo BUILD_TAG_NETWORK=envnetworkcustom >> $GITHUB_ENV
echo VERSION=${VERSION} >> $GITHUB_ENV
echo GOPRIVATE=github.com/anyproto >> $GITHUB_ENV
echo $(pwd)/deps >> $GITHUB_PATH
git config --global url."https://${{ secrets.ANYTYPE_PAT }}@github.com/".insteadOf "https://github.com/"
- name: Go mod download
run: go mod download
- name: install protoc
run: make setup-protoc
- name: setup go
run: |
make setup-go
make setup-gomobile
which gomobile
- name: Cross-compile library mac/win/linux
run: |
echo $FLAGS
mkdir -p .release
echo $SDKROOT
GOOS="darwin" CGO_CFLAGS="-mmacosx-version-min=11" MACOSX_DEPLOYMENT_TARGET=11.0 GOARCH="amd64" CGO_ENABLED="1" go build -tags="$BUILD_TAG_NETWORK nographviz nowatchdog nosigar nomutexdeadlockdetector" -ldflags="$FLAGS" -o darwin-amd64 github.com/anyproto/anytype-heart/cmd/grpcserver
export SDKROOT=$(xcrun --sdk macosx --show-sdk-path)
echo $SDKROOT
GOOS="darwin" CGO_CFLAGS="-mmacosx-version-min=11" MACOSX_DEPLOYMENT_TARGET=11.0 GOARCH="arm64" CGO_ENABLED="1" go build -tags="$BUILD_TAG_NETWORK nographviz nowatchdog nosigar nomutexdeadlockdetector" -ldflags="$FLAGS" -o darwin-arm64 github.com/anyproto/anytype-heart/cmd/grpcserver
GOOS="windows" GOARCH="amd64" CGO_ENABLED="1" CC="x86_64-w64-mingw32-gcc" CXX="x86_64-w64-mingw32-g++" go build -tags="$BUILD_TAG_NETWORK nographviz nowatchdog nosigar nomutexdeadlockdetector noheic" -ldflags="$FLAGS -linkmode external -extldflags=-static" -o windows-amd64 github.com/anyproto/anytype-heart/cmd/grpcserver
GOOS="linux" GOARCH="amd64" CGO_ENABLED="1" CC="x86_64-linux-musl-gcc" go build -tags="$BUILD_TAG_NETWORK nographviz nowatchdog nosigar nomutexdeadlockdetector noheic" -ldflags="$FLAGS -linkmode external -extldflags '-static -Wl,-z stack-size=1000000'" -o linux-amd64 github.com/anyproto/anytype-heart/cmd/grpcserver
GOOS="linux" GOARCH="arm64" CGO_ENABLED="1" CC="aarch64-linux-musl-gcc" go build -tags="$BUILD_TAG_NETWORK nographviz nowatchdog nosigar nomutexdeadlockdetector noheic" -ldflags="$FLAGS -linkmode external" -o linux-arm64 github.com/anyproto/anytype-heart/cmd/grpcserver
ls -lha .
- name: Make JS protos
run: |
make protos-js
mv dist/js/pb protobuf
mkdir -p protobuf/protos
cp pb/protos/*.proto ./protobuf/protos
cp pb/protos/service/*.proto ./protobuf/protos
cp pkg/lib/pb/model/protos/*.proto ./protobuf/protos
- name: Add system relations/types jsons
run: |
mkdir -p json/
cp pkg/lib/bundle/systemRelations.json ./json
cp pkg/lib/bundle/systemTypes.json ./json
cp pkg/lib/bundle/internalRelations.json ./json
cp pkg/lib/bundle/internalTypes.json ./json
- name: Pack server win
run: |
declare -a arr=("windows-amd64")
for i in "${arr[@]}"; do
OSARCH=${i%.*}
cp ./${i}* ./grpc-server.exe
zip -r js_${VERSION}_${OSARCH}.zip grpc-server.exe protobuf json
mv js_${VERSION}_${OSARCH}.zip .release/
done
- name: Pack server unix
run: |
declare -a arr=("darwin-amd64" "darwin-arm64" "linux-amd64")
for i in "${arr[@]}"; do
OSARCH=${i%.*}
cp ./${i}* ./grpc-server
tar -czf js_${VERSION}_${OSARCH}.tar.gz grpc-server protobuf json
mv js_${VERSION}_${OSARCH}.tar.gz .release/
done
- name: Make swift protos
run: |
mkdir -p .release
make protos-swift
rm -rf protobuf
mv dist/ios/protobuf protobuf
mkdir -p protobuf/protos
cp pb/protos/*.proto ./protobuf/protos
cp pb/protos/service/*.proto ./protobuf/protos
cp pkg/lib/pb/model/protos/*.proto ./protobuf/protos
- name: Add system relations/types jsons
run: |
mkdir -p json/
cp pkg/lib/bundle/systemRelations.json ./json
cp pkg/lib/bundle/relations.json ./json
cp pkg/lib/bundle/systemTypes.json ./json
cp pkg/lib/bundle/internalRelations.json ./json
cp pkg/lib/bundle/internalTypes.json ./json
- name: Compile ios lib
run: |
go install github.com/vektra/mockery/v2@v2.47.0
go install go.uber.org/mock/mockgen@v0.5.0
make test-deps
gomobile bind -tags "$BUILD_TAG_NETWORK nogrpcserver gomobile nowatchdog nosigar nomutexdeadlockdetector timetzdata rasterizesvg" -ldflags "$FLAGS" -v -target=ios -o Lib.xcframework github.com/anyproto/anytype-heart/clientlibrary/service github.com/anyproto/anytype-heart/core || true
mkdir -p dist/ios/ && mv Lib.xcframework dist/ios/
go run cmd/iosrepack/main.go
mv dist/ios/Lib.xcframework .
gtar --exclude ".*" -czvf ios_framework.tar.gz Lib.xcframework protobuf json
#gradle publish
mv ios_framework.tar.gz .release/ios_framework_${VERSION}.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_USER: ${{ github.actor }}
- name: Make java protos
run: |
make protos-java
rm -rf protobuf
mv dist/android/pb protobuf
mkdir -p protobuf/protos
cp pb/protos/*.proto ./protobuf/protos
cp pb/protos/service/*.proto ./protobuf/protos
cp pkg/lib/pb/model/protos/*.proto ./protobuf/protos
- name: Add system relations/types jsons
run: |
mkdir -p json/
cp pkg/lib/bundle/systemRelations.json ./json
cp pkg/lib/bundle/systemTypes.json ./json
cp pkg/lib/bundle/internalRelations.json ./json
cp pkg/lib/bundle/internalTypes.json ./json
- name: Compile android lib
run: |
gomobile bind -tags "$BUILD_TAG_NETWORK nogrpcserver gomobile nowatchdog nosigar nomutexdeadlockdetector timetzdata rasterizesvg" -ldflags "$FLAGS" -v -target=android -androidapi 26 -o lib.aar github.com/anyproto/anytype-heart/clientlibrary/service github.com/anyproto/anytype-heart/core || true
gtar --exclude ".*" -czvf android_lib_${VERSION}.tar.gz lib.aar protobuf json
mv android_lib_${VERSION}.tar.gz .release/
# upload release artifacts to s3 {{
- name: Install AWS CLI
run: |
if ! which aws; then
brew install awscli
fi
aws --version
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.NIGHTLY_AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.NIGHTLY_AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.NIGHTLY_AWS_REGION }}
- name: Upload build artifacts to S3
run: |
aws s3 cp .release/ s3://${{ env.S3_BUCKET }}/mw/ --recursive --acl public-read
# }}

View file

@ -19,6 +19,7 @@ packages:
github.com/anyproto/anytype-heart/core/block/cache:
interfaces:
ObjectGetter:
CachedObjectGetter:
ObjectGetterComponent:
github.com/anyproto/anytype-heart/pkg/lib/core:
interfaces:
@ -37,9 +38,6 @@ packages:
dir: "{{.InterfaceDir}}"
outpkg: "{{.PackageName}}"
inpackage: true
github.com/anyproto/anytype-heart/core/block/editor/lastused:
interfaces:
ObjectUsageUpdater:
github.com/anyproto/anytype-heart/core/block/import/common:
interfaces:
Converter:
@ -231,3 +229,6 @@ packages:
github.com/anyproto/anytype-heart/core/identity:
interfaces:
Service:
github.com/anyproto/anytype-heart/pb/service:
interfaces:
ClientCommandsServer:

View file

@ -10,13 +10,13 @@ export GOLANGCI_LINT_VERSION=1.58.1
export CGO_CFLAGS=-Wno-deprecated-non-prototype -Wno-unknown-warning-option -Wno-deprecated-declarations -Wno-xor-used-as-pow -Wno-single-bit-bitfield-constant-conversion
ifndef $(GOPATH)
GOPATH=$(shell go env GOPATH)
export GOPATH
GOPATH=$(shell go env GOPATH)
export GOPATH
endif
ifndef $(GOROOT)
GOROOT=$(shell go env GOROOT)
export GOROOT
GOROOT=$(shell go env GOROOT)
export GOROOT
endif
DEPS_PATH := $(shell pwd)/deps
@ -38,14 +38,14 @@ ifdef ANYENV
@exit 1;
endif
@if [ -z "$$ANY_SYNC_NETWORK" ]; then \
echo "Using the default production Any Sync Network"; \
elif [ ! -e "$$ANY_SYNC_NETWORK" ]; then \
echo "Network configuration file not found at $$ANY_SYNC_NETWORK"; \
exit 1; \
else \
echo "Using Any Sync Network configuration at $$ANY_SYNC_NETWORK"; \
cp $$ANY_SYNC_NETWORK $(CUSTOM_NETWORK_FILE); \
fi
echo "Using the default production Any Sync Network"; \
elif [ ! -e "$$ANY_SYNC_NETWORK" ]; then \
echo "Network configuration file not found at $$ANY_SYNC_NETWORK"; \
exit 1; \
else \
echo "Using Any Sync Network configuration at $$ANY_SYNC_NETWORK"; \
cp $$ANY_SYNC_NETWORK $(CUSTOM_NETWORK_FILE); \
fi
setup-go: setup-network-config check-tantivy-version
@echo 'Setting up go modules...'
@ -102,7 +102,7 @@ build-js-addon:
@cp dist/lib.a clientlibrary/jsaddon/lib.a
@cp dist/lib.h clientlibrary/jsaddon/lib.h
@cp clientlibrary/clib/bridge.h clientlibrary/jsaddon/bridge.h
# Electron's version.
# Electron's version.
@export npm_config_target=12.0.4
@export npm_config_arch=x64
@export npm_config_target_arch=x64
@ -197,7 +197,6 @@ setup-protoc-go:
go build -o deps github.com/gogo/protobuf/protoc-gen-gogofast
go build -o deps github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc
setup-protoc-jsweb:
@echo 'Installing grpc-web plugin...'
@rm -rf deps/grpc-web
@ -346,17 +345,17 @@ OUTPUT_DIR := deps/libs
SHA_FILE = tantivity_sha256.txt
TANTIVY_LIBS := android-386.tar.gz \
android-amd64.tar.gz \
android-arm.tar.gz \
android-arm64.tar.gz \
darwin-amd64.tar.gz \
darwin-arm64.tar.gz \
ios-amd64.tar.gz \
ios-arm64.tar.gz \
ios-arm64-sim.tar.gz \
linux-amd64-musl.tar.gz \
linux-arm64-musl.tar.gz \
windows-amd64.tar.gz
android-amd64.tar.gz \
android-arm.tar.gz \
android-arm64.tar.gz \
darwin-amd64.tar.gz \
darwin-arm64.tar.gz \
ios-amd64.tar.gz \
ios-arm64.tar.gz \
ios-arm64-sim.tar.gz \
linux-amd64-musl.tar.gz \
linux-arm64-musl.tar.gz \
windows-amd64.tar.gz
define download_tantivy_lib
curl -L -o $(OUTPUT_DIR)/$(1) https://github.com/$(REPO)/releases/download/$(TANTIVY_VERSION)/$(1)
@ -404,4 +403,4 @@ check-tantivy-version:
$(eval OLD_VERSION := $(shell [ -f $(OUTPUT_DIR)/.verified ] && cat $(OUTPUT_DIR)/.verified || echo ""))
@if [ "$(TANTIVY_VERSION)" != "$(OLD_VERSION)" ]; then \
$(MAKE) download-tantivy-all; \
fi
fi

1
account_stop.json Normal file
View file

@ -0,0 +1 @@
{"method_name":"AccountStop","middle_time":0,"error_code":101,"description":"application is not running"}

View file

@ -25,344 +25,348 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
func init() { proto.RegisterFile("pb/protos/service/service.proto", fileDescriptor_93a29dc403579097) }
var fileDescriptor_93a29dc403579097 = []byte{
// 5387 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x9d, 0xdf, 0x6f, 0x24, 0x49,
0x52, 0xf8, 0xd7, 0x2f, 0xdf, 0xfd, 0x52, 0xc7, 0x2d, 0xd0, 0x0b, 0xcb, 0xde, 0x72, 0x37, 0xbf,
0x76, 0xc6, 0xf6, 0x8c, 0xed, 0xf6, 0xec, 0xcc, 0xce, 0xee, 0xe9, 0x0e, 0x09, 0x79, 0xec, 0xb1,
0xd7, 0x9c, 0xed, 0x31, 0xee, 0xf6, 0x8c, 0xb4, 0x12, 0x12, 0xe9, 0xea, 0x74, 0xbb, 0x70, 0x75,
0x65, 0x5d, 0x55, 0x76, 0x7b, 0xfa, 0x10, 0x08, 0x04, 0x02, 0x81, 0x40, 0x9c, 0xf8, 0xf5, 0x8a,
0xc4, 0x5f, 0xc3, 0xe3, 0x3d, 0xf2, 0x88, 0x76, 0xdf, 0xf9, 0x0f, 0x90, 0x50, 0x65, 0x65, 0xe5,
0x8f, 0xa8, 0x88, 0xac, 0xf2, 0x3d, 0xcd, 0xc8, 0xf1, 0x89, 0x88, 0xcc, 0xca, 0xc8, 0xc8, 0xc8,
0xac, 0x1f, 0x1d, 0xdd, 0xcd, 0x2f, 0xb6, 0xf3, 0x42, 0x48, 0x51, 0x6e, 0x97, 0xbc, 0x58, 0x24,
0x31, 0x6f, 0xfe, 0x1d, 0xaa, 0x3f, 0x0f, 0xde, 0x67, 0xd9, 0x52, 0x2e, 0x73, 0xfe, 0xc9, 0xc7,
0x96, 0x8c, 0xc5, 0x6c, 0xc6, 0xb2, 0x49, 0x59, 0x23, 0x9f, 0x7c, 0x64, 0x25, 0x7c, 0xc1, 0x33,
0xa9, 0xff, 0xfe, 0xec, 0x7f, 0xff, 0x67, 0x25, 0xfa, 0x60, 0x37, 0x4d, 0x78, 0x26, 0x77, 0xb5,
0xc6, 0xe0, 0xeb, 0xe8, 0xbb, 0x3b, 0x79, 0x7e, 0xc0, 0xe5, 0x1b, 0x5e, 0x94, 0x89, 0xc8, 0x06,
0x9f, 0x0e, 0xb5, 0x83, 0xe1, 0x59, 0x1e, 0x0f, 0x77, 0xf2, 0x7c, 0x68, 0x85, 0xc3, 0x33, 0xfe,
0xd3, 0x39, 0x2f, 0xe5, 0x27, 0x0f, 0xc3, 0x50, 0x99, 0x8b, 0xac, 0xe4, 0x83, 0xcb, 0xe8, 0x37,
0x76, 0xf2, 0x7c, 0xc4, 0xe5, 0x1e, 0xaf, 0x3a, 0x30, 0x92, 0x4c, 0xf2, 0xc1, 0x5a, 0x4b, 0xd5,
0x07, 0x8c, 0x8f, 0xf5, 0x6e, 0x50, 0xfb, 0x19, 0x47, 0xdf, 0xa9, 0xfc, 0x5c, 0xcd, 0xe5, 0x44,
0xdc, 0x64, 0x83, 0xfb, 0x6d, 0x45, 0x2d, 0x32, 0xb6, 0x1f, 0x84, 0x10, 0x6d, 0xf5, 0x6d, 0xf4,
0xab, 0x6f, 0x59, 0x9a, 0x72, 0xb9, 0x5b, 0xf0, 0xaa, 0xe1, 0xbe, 0x4e, 0x2d, 0x1a, 0xd6, 0x32,
0x63, 0xf7, 0xd3, 0x20, 0xa3, 0x0d, 0x7f, 0x1d, 0x7d, 0xb7, 0x96, 0x9c, 0xf1, 0x58, 0x2c, 0x78,
0x31, 0x40, 0xb5, 0xb4, 0x90, 0xb8, 0xe4, 0x2d, 0x08, 0xda, 0xde, 0x15, 0xd9, 0x82, 0x17, 0x12,
0xb7, 0xad, 0x85, 0x61, 0xdb, 0x16, 0xd2, 0xb6, 0xff, 0x76, 0x25, 0xfa, 0xfe, 0x4e, 0x1c, 0x8b,
0x79, 0x26, 0x8f, 0x44, 0xcc, 0xd2, 0xa3, 0x24, 0xbb, 0x3e, 0xe1, 0x37, 0xbb, 0x57, 0x15, 0x9f,
0x4d, 0xf9, 0xe0, 0xb9, 0x7f, 0x55, 0x6b, 0x74, 0x68, 0xd8, 0xa1, 0x0b, 0x1b, 0xdf, 0x9f, 0xdf,
0x4e, 0x49, 0xb7, 0xe5, 0x1f, 0x57, 0xa2, 0x3b, 0xb0, 0x2d, 0x23, 0x91, 0x2e, 0xb8, 0x6d, 0xcd,
0x8b, 0x0e, 0xc3, 0x3e, 0x6e, 0xda, 0xf3, 0xc5, 0x6d, 0xd5, 0x74, 0x8b, 0xd2, 0xe8, 0x43, 0x37,
0x5c, 0x46, 0xbc, 0x54, 0xd3, 0xe9, 0x31, 0x1d, 0x11, 0x1a, 0x31, 0x9e, 0x9f, 0xf4, 0x41, 0xb5,
0xb7, 0x24, 0x1a, 0x68, 0x6f, 0xa9, 0x28, 0x8d, 0xb3, 0x75, 0xd4, 0x82, 0x43, 0x18, 0x5f, 0x8f,
0x7b, 0x90, 0xda, 0xd5, 0x1f, 0x45, 0xbf, 0xf6, 0x56, 0x14, 0xd7, 0x65, 0xce, 0x62, 0xae, 0xa7,
0xc2, 0x23, 0x5f, 0xbb, 0x91, 0xc2, 0xd9, 0xb0, 0xda, 0x85, 0x39, 0x41, 0xdb, 0x08, 0x5f, 0xe7,
0x1c, 0xe6, 0x20, 0xab, 0x58, 0x09, 0xa9, 0xa0, 0x85, 0x90, 0xb6, 0x7d, 0x1d, 0x0d, 0xac, 0xed,
0x8b, 0x3f, 0xe6, 0xb1, 0xdc, 0x99, 0x4c, 0xe0, 0xa8, 0x58, 0x5d, 0x45, 0x0c, 0x77, 0x26, 0x13,
0x6a, 0x54, 0x70, 0x54, 0x3b, 0xbb, 0x89, 0x3e, 0x02, 0xce, 0x8e, 0x92, 0x52, 0x39, 0xdc, 0x0a,
0x5b, 0xd1, 0x98, 0x71, 0x3a, 0xec, 0x8b, 0x6b, 0xc7, 0x7f, 0xbe, 0x12, 0x7d, 0x0f, 0xf1, 0x7c,
0xc6, 0x67, 0x62, 0xc1, 0x07, 0x4f, 0xbb, 0xad, 0xd5, 0xa4, 0xf1, 0xff, 0xd9, 0x2d, 0x34, 0x90,
0x30, 0x19, 0xf1, 0x94, 0xc7, 0x92, 0x0c, 0x93, 0x5a, 0xdc, 0x19, 0x26, 0x06, 0x73, 0x66, 0x58,
0x23, 0x3c, 0xe0, 0x72, 0x77, 0x5e, 0x14, 0x3c, 0x93, 0xe4, 0x58, 0x5a, 0xa4, 0x73, 0x2c, 0x3d,
0x14, 0xe9, 0xcf, 0x01, 0x97, 0x3b, 0x69, 0x4a, 0xf6, 0xa7, 0x16, 0x77, 0xf6, 0xc7, 0x60, 0xda,
0x43, 0x1c, 0xfd, 0xba, 0x73, 0xc5, 0xe4, 0x61, 0x76, 0x29, 0x06, 0xf4, 0xb5, 0x50, 0x72, 0xe3,
0x63, 0xad, 0x93, 0x43, 0xba, 0xf1, 0xea, 0x5d, 0x2e, 0x0a, 0x7a, 0x58, 0x6a, 0x71, 0x67, 0x37,
0x0c, 0xa6, 0x3d, 0xfc, 0x61, 0xf4, 0x81, 0xce, 0x92, 0xcd, 0x7a, 0xf6, 0x10, 0x4d, 0xa1, 0x70,
0x41, 0x7b, 0xd4, 0x41, 0xd9, 0xe4, 0xa0, 0x65, 0x3a, 0xf9, 0x7c, 0x8a, 0xea, 0x81, 0xd4, 0xf3,
0x30, 0x0c, 0xb5, 0x6c, 0xef, 0xf1, 0x94, 0x93, 0xb6, 0x6b, 0x61, 0x87, 0x6d, 0x03, 0x69, 0xdb,
0x45, 0xf4, 0x5b, 0xe6, 0xb2, 0x54, 0xeb, 0xa8, 0x92, 0x57, 0x49, 0x7a, 0x83, 0xe8, 0xb7, 0x0b,
0x19, 0x5f, 0x9b, 0xfd, 0xe0, 0x56, 0x7f, 0xf4, 0x0c, 0xc4, 0xfb, 0x03, 0xe6, 0xdf, 0xc3, 0x30,
0xa4, 0x6d, 0xff, 0xdd, 0x4a, 0xf4, 0x03, 0x2d, 0x7b, 0x95, 0xb1, 0x8b, 0x94, 0xab, 0x25, 0xf1,
0x84, 0xcb, 0x1b, 0x51, 0x5c, 0x8f, 0x96, 0x59, 0x4c, 0x2c, 0xff, 0x38, 0xdc, 0xb1, 0xfc, 0x93,
0x4a, 0x4e, 0xc5, 0xa7, 0x3b, 0x2a, 0x45, 0x0e, 0x2b, 0xbe, 0xa6, 0x07, 0x52, 0xe4, 0x54, 0xc5,
0xe7, 0x23, 0x2d, 0xab, 0xc7, 0x55, 0xda, 0xc4, 0xad, 0x1e, 0xbb, 0x79, 0xf2, 0x41, 0x08, 0xb1,
0x69, 0xab, 0x09, 0x60, 0x91, 0x5d, 0x26, 0xd3, 0xf3, 0x7c, 0x52, 0x85, 0xf1, 0x63, 0x3c, 0x42,
0x1d, 0x84, 0x48, 0x5b, 0x04, 0xaa, 0xbd, 0xfd, 0x83, 0x2d, 0x8c, 0xf4, 0x54, 0xda, 0x2f, 0xc4,
0xec, 0x88, 0x4f, 0x59, 0xbc, 0xd4, 0xf3, 0xff, 0xf3, 0xd0, 0xc4, 0x83, 0xb4, 0x69, 0xc4, 0x8b,
0x5b, 0x6a, 0xe9, 0xf6, 0xfc, 0xfb, 0x4a, 0xf4, 0xb0, 0xe9, 0xfe, 0x15, 0xcb, 0xa6, 0x5c, 0x8f,
0x67, 0xdd, 0xfa, 0x9d, 0x6c, 0x72, 0xc6, 0x4b, 0xc9, 0x0a, 0x39, 0xf8, 0x11, 0xde, 0xc9, 0x90,
0x8e, 0x69, 0xdb, 0x8f, 0x7f, 0x29, 0x5d, 0x3b, 0xea, 0xa3, 0x2a, 0xb1, 0xe9, 0x14, 0xe0, 0x8f,
0xba, 0x92, 0xc0, 0x04, 0xf0, 0x20, 0x84, 0xd8, 0x51, 0x57, 0x82, 0xc3, 0x6c, 0x91, 0x48, 0x7e,
0xc0, 0x33, 0x5e, 0xb4, 0x47, 0xbd, 0x56, 0xf5, 0x11, 0x62, 0xd4, 0x09, 0xd4, 0x26, 0x1b, 0xcf,
0x9b, 0x59, 0x1c, 0x37, 0x02, 0x46, 0x5a, 0xcb, 0xe3, 0x66, 0x3f, 0xd8, 0xee, 0xee, 0x1c, 0x9f,
0x67, 0x7c, 0x21, 0xae, 0xe1, 0xee, 0xce, 0x35, 0x51, 0x03, 0xc4, 0xee, 0x0e, 0x05, 0xed, 0x0a,
0xe6, 0xf8, 0x79, 0x93, 0xf0, 0x1b, 0xb0, 0x82, 0xb9, 0xca, 0x95, 0x98, 0x58, 0xc1, 0x10, 0x4c,
0x7b, 0x38, 0x89, 0x7e, 0x45, 0x09, 0x7f, 0x5f, 0x24, 0xd9, 0xe0, 0x2e, 0xa2, 0x54, 0x09, 0x8c,
0xd5, 0x7b, 0x34, 0x00, 0x5a, 0x5c, 0xfd, 0x75, 0x97, 0x65, 0x31, 0x4f, 0xd1, 0x16, 0x5b, 0x71,
0xb0, 0xc5, 0x1e, 0x66, 0x4b, 0x07, 0x25, 0xac, 0xf2, 0xd7, 0xe8, 0x8a, 0x15, 0x49, 0x36, 0x1d,
0x60, 0xba, 0x8e, 0x9c, 0x28, 0x1d, 0x30, 0x0e, 0x84, 0xb0, 0x56, 0xdc, 0xc9, 0xf3, 0xa2, 0x4a,
0x8b, 0x58, 0x08, 0xfb, 0x48, 0x30, 0x84, 0x5b, 0x28, 0xee, 0x6d, 0x8f, 0xc7, 0x69, 0x92, 0x05,
0xbd, 0x69, 0xa4, 0x8f, 0x37, 0x8b, 0x82, 0xe0, 0x3d, 0xe2, 0x6c, 0xc1, 0x9b, 0x9e, 0x61, 0x57,
0xc6, 0x05, 0x82, 0xc1, 0x0b, 0x40, 0xbb, 0x4f, 0x53, 0xe2, 0x63, 0x76, 0xcd, 0xab, 0x0b, 0xcc,
0xab, 0x75, 0x6d, 0x80, 0xe9, 0x7b, 0x04, 0xb1, 0x4f, 0xc3, 0x49, 0xed, 0x6a, 0x1e, 0x7d, 0xa4,
0xe4, 0xa7, 0xac, 0x90, 0x49, 0x9c, 0xe4, 0x2c, 0x6b, 0xea, 0x7f, 0x6c, 0x5e, 0xb7, 0x28, 0xe3,
0x72, 0xab, 0x27, 0xad, 0xdd, 0xfe, 0xdb, 0x4a, 0x74, 0x1f, 0xfa, 0x3d, 0xe5, 0xc5, 0x2c, 0x51,
0xdb, 0xc8, 0xb2, 0x4e, 0xc2, 0x83, 0x2f, 0xc3, 0x46, 0x5b, 0x0a, 0xa6, 0x35, 0x3f, 0xbc, 0xbd,
0xa2, 0x2d, 0x86, 0x46, 0xba, 0xb4, 0x7e, 0x5d, 0x4c, 0x5a, 0xc7, 0x2c, 0xa3, 0xa6, 0x5e, 0x56,
0x42, 0xa2, 0x18, 0x6a, 0x41, 0x60, 0x86, 0x9f, 0x67, 0x65, 0x63, 0x1d, 0x9b, 0xe1, 0x56, 0x1c,
0x9c, 0xe1, 0x1e, 0x66, 0x67, 0xf8, 0xe9, 0xfc, 0x22, 0x4d, 0xca, 0xab, 0x24, 0x9b, 0xea, 0xca,
0xd7, 0xd7, 0xb5, 0x62, 0x58, 0xfc, 0xae, 0x75, 0x72, 0x98, 0x13, 0x1d, 0x2c, 0xa4, 0x13, 0x10,
0x26, 0x6b, 0x9d, 0x9c, 0xdd, 0x1f, 0x58, 0x69, 0xb5, 0x73, 0x04, 0xfb, 0x03, 0x47, 0xb5, 0x92,
0x12, 0xfb, 0x83, 0x36, 0xa5, 0xcd, 0x8b, 0xe8, 0x37, 0xdd, 0x3e, 0x94, 0x22, 0x5d, 0xf0, 0xf3,
0x22, 0x19, 0x3c, 0xa1, 0xdb, 0xd7, 0x30, 0xc6, 0xd5, 0x46, 0x2f, 0xd6, 0x26, 0x2a, 0x4b, 0x1c,
0x70, 0x39, 0x92, 0x4c, 0xce, 0x4b, 0x90, 0xa8, 0x1c, 0x1b, 0x06, 0x21, 0x12, 0x15, 0x81, 0x6a,
0x6f, 0x7f, 0x10, 0x45, 0xf5, 0xa6, 0x5b, 0x1d, 0x8c, 0xf8, 0x6b, 0x8f, 0xde, 0x8d, 0x7b, 0xa7,
0x22, 0xf7, 0x03, 0x84, 0x2d, 0x78, 0xea, 0xbf, 0xab, 0xf3, 0x9e, 0x01, 0xaa, 0xa1, 0x44, 0x44,
0xc1, 0x03, 0x10, 0xd8, 0xd0, 0xd1, 0x95, 0xb8, 0xc1, 0x1b, 0x5a, 0x49, 0xc2, 0x0d, 0xd5, 0x84,
0x3d, 0x81, 0xd5, 0x0d, 0xc5, 0x4e, 0x60, 0x9b, 0x66, 0x84, 0x4e, 0x60, 0x21, 0x63, 0x63, 0xc6,
0x35, 0xfc, 0x52, 0x88, 0xeb, 0x19, 0x2b, 0xae, 0x41, 0xcc, 0x78, 0xca, 0x0d, 0x43, 0xc4, 0x0c,
0xc5, 0xda, 0x98, 0x71, 0x1d, 0x56, 0xe5, 0xf2, 0x79, 0x91, 0x82, 0x98, 0xf1, 0x6c, 0x68, 0x84,
0x88, 0x19, 0x02, 0xb5, 0xd9, 0xc9, 0xf5, 0x36, 0xe2, 0x70, 0xcf, 0xef, 0xa9, 0x8f, 0x38, 0xb5,
0xe7, 0x47, 0x30, 0x18, 0x42, 0x07, 0x05, 0xcb, 0xaf, 0xf0, 0x10, 0x52, 0xa2, 0x70, 0x08, 0x35,
0x08, 0x1c, 0xef, 0x11, 0x67, 0x45, 0x7c, 0x85, 0x8f, 0x77, 0x2d, 0x0b, 0x8f, 0xb7, 0x61, 0xe0,
0x78, 0xd7, 0x82, 0xb7, 0x89, 0xbc, 0x3a, 0xe6, 0x92, 0xe1, 0xe3, 0xed, 0x33, 0xe1, 0xf1, 0x6e,
0xb1, 0xb6, 0x1e, 0x77, 0x1d, 0x8e, 0xe6, 0x17, 0x65, 0x5c, 0x24, 0x17, 0x7c, 0x10, 0xb0, 0x62,
0x20, 0xa2, 0x1e, 0x27, 0x61, 0xed, 0xf3, 0xe7, 0x2b, 0xd1, 0xdd, 0x66, 0xd8, 0x45, 0x59, 0xea,
0xb5, 0xcf, 0x77, 0xff, 0x02, 0x1f, 0x5f, 0x02, 0x27, 0xce, 0xc4, 0x7b, 0xa8, 0x39, 0xb5, 0x01,
0xde, 0xa4, 0xf3, 0xac, 0x34, 0x8d, 0xfa, 0xb2, 0x8f, 0x75, 0x47, 0x81, 0xa8, 0x0d, 0x7a, 0x29,
0xda, 0xb2, 0x4c, 0x8f, 0x4f, 0x23, 0x3b, 0x9c, 0x94, 0xa0, 0x2c, 0x6b, 0xae, 0xb7, 0x43, 0x10,
0x65, 0x19, 0x4e, 0xc2, 0x50, 0x38, 0x28, 0xc4, 0x3c, 0x2f, 0x3b, 0x42, 0x01, 0x40, 0xe1, 0x50,
0x68, 0xc3, 0xda, 0xe7, 0xbb, 0xe8, 0xb7, 0xdd, 0xf0, 0x73, 0x2f, 0xf6, 0x16, 0x1d, 0x53, 0xd8,
0x25, 0x1e, 0xf6, 0xc5, 0x6d, 0x45, 0xd1, 0x78, 0x96, 0x7b, 0x5c, 0xb2, 0x24, 0x2d, 0x07, 0xab,
0xb8, 0x8d, 0x46, 0x4e, 0x54, 0x14, 0x18, 0x07, 0xf3, 0xdb, 0xde, 0x3c, 0x4f, 0x93, 0xb8, 0x7d,
0x47, 0x42, 0xeb, 0x1a, 0x71, 0x38, 0xbf, 0xb9, 0x18, 0xcc, 0xd7, 0x55, 0xe9, 0xa7, 0xfe, 0x33,
0x5e, 0xe6, 0x1c, 0xcf, 0xd7, 0x1e, 0x12, 0xce, 0xd7, 0x10, 0x85, 0xfd, 0x19, 0x71, 0x79, 0xc4,
0x96, 0x62, 0x4e, 0xe4, 0x6b, 0x23, 0x0e, 0xf7, 0xc7, 0xc5, 0xec, 0xde, 0xc0, 0x78, 0x38, 0xcc,
0x24, 0x2f, 0x32, 0x96, 0xee, 0xa7, 0x6c, 0x5a, 0x0e, 0x88, 0x1c, 0xe3, 0x53, 0xc4, 0xde, 0x80,
0xa6, 0x91, 0xcb, 0x78, 0x58, 0xee, 0xb3, 0x85, 0x28, 0x12, 0x49, 0x5f, 0x46, 0x8b, 0x74, 0x5e,
0x46, 0x0f, 0x45, 0xbd, 0xed, 0x14, 0xf1, 0x55, 0xb2, 0xe0, 0x93, 0x80, 0xb7, 0x06, 0xe9, 0xe1,
0xcd, 0x41, 0x91, 0x41, 0x1b, 0x89, 0x79, 0x11, 0x73, 0x72, 0xd0, 0x6a, 0x71, 0xe7, 0xa0, 0x19,
0x4c, 0x7b, 0xf8, 0xab, 0x95, 0xe8, 0x77, 0x6a, 0xa9, 0x7b, 0x9b, 0x60, 0x8f, 0x95, 0x57, 0x17,
0x82, 0x15, 0x93, 0xc1, 0x67, 0x98, 0x1d, 0x14, 0x35, 0xae, 0x9f, 0xdd, 0x46, 0x05, 0x5e, 0xd6,
0xaa, 0xee, 0xb6, 0x33, 0x0e, 0xbd, 0xac, 0x1e, 0x12, 0xbe, 0xac, 0x10, 0x85, 0x09, 0x44, 0xc9,
0xeb, 0x23, 0xb9, 0x55, 0x52, 0xdf, 0x3f, 0x97, 0x5b, 0xeb, 0xe4, 0x60, 0x7e, 0xac, 0x84, 0x7e,
0xb4, 0x6c, 0x51, 0x36, 0xf0, 0x88, 0x19, 0xf6, 0xc5, 0x49, 0xcf, 0x66, 0x56, 0x84, 0x3d, 0xb7,
0x66, 0xc6, 0xb0, 0x2f, 0x4e, 0x78, 0x76, 0xd2, 0x5a, 0xc8, 0x33, 0x92, 0xda, 0x86, 0x7d, 0x71,
0x58, 0x7d, 0x69, 0xa6, 0x59, 0x17, 0x9e, 0x04, 0xec, 0xc0, 0xb5, 0x61, 0xa3, 0x17, 0xab, 0x1d,
0xfe, 0xcd, 0x4a, 0xf4, 0x7d, 0xeb, 0xf1, 0x58, 0x4c, 0x92, 0xcb, 0x65, 0x0d, 0xbd, 0x61, 0xe9,
0x9c, 0x97, 0x83, 0x67, 0x94, 0xb5, 0x36, 0x6b, 0x5a, 0xf0, 0xfc, 0x56, 0x3a, 0x70, 0xee, 0xec,
0xe4, 0x79, 0xba, 0x1c, 0xf3, 0x59, 0x9e, 0x92, 0x73, 0xc7, 0x43, 0xc2, 0x73, 0x07, 0xa2, 0xb0,
0x2a, 0x1f, 0x8b, 0xaa, 0xe6, 0x47, 0xab, 0x72, 0x25, 0x0a, 0x57, 0xe5, 0x0d, 0x02, 0x6b, 0xa5,
0xb1, 0xd8, 0x15, 0x69, 0xca, 0x63, 0xd9, 0x7e, 0xd4, 0xc0, 0x68, 0x5a, 0x22, 0x5c, 0x2b, 0x01,
0xd2, 0x9e, 0xca, 0x35, 0x7b, 0x48, 0x56, 0xf0, 0x97, 0xcb, 0xa3, 0x24, 0xbb, 0x1e, 0xe0, 0x65,
0x81, 0x05, 0x88, 0x53, 0x39, 0x14, 0x84, 0x7b, 0xd5, 0xf3, 0x6c, 0x22, 0xf0, 0xbd, 0x6a, 0x25,
0x09, 0xef, 0x55, 0x35, 0x01, 0x4d, 0x9e, 0x71, 0xca, 0x64, 0x25, 0x09, 0x9b, 0xd4, 0x04, 0x96,
0x0a, 0xf5, 0xbd, 0x1b, 0x32, 0x15, 0x82, 0xbb, 0x35, 0x6b, 0x9d, 0x1c, 0x8c, 0xd0, 0x66, 0xd3,
0xba, 0xcf, 0x65, 0x7c, 0x85, 0x47, 0xa8, 0x87, 0x84, 0x23, 0x14, 0xa2, 0xb0, 0x4b, 0x63, 0x61,
0x36, 0xdd, 0xab, 0x78, 0x7c, 0xb4, 0x36, 0xdc, 0x6b, 0x9d, 0x1c, 0xdc, 0x46, 0x1e, 0xce, 0xd4,
0x35, 0x43, 0x83, 0xbc, 0x96, 0x85, 0xb7, 0x91, 0x86, 0x81, 0xad, 0xaf, 0x05, 0xea, 0x2c, 0x6b,
0x95, 0x56, 0xf4, 0x4e, 0xb3, 0xd6, 0x3a, 0x39, 0xed, 0xe4, 0x5f, 0xcc, 0x36, 0xae, 0x96, 0x9e,
0x88, 0x6a, 0x8e, 0xbc, 0x61, 0x69, 0x32, 0x61, 0x92, 0x8f, 0xc5, 0x35, 0xcf, 0xf0, 0x1d, 0x93,
0x6e, 0x6d, 0xcd, 0x0f, 0x3d, 0x85, 0xf0, 0x8e, 0x29, 0xac, 0x08, 0xe3, 0xa4, 0xa6, 0xcf, 0x4b,
0xbe, 0xcb, 0x4a, 0x22, 0x93, 0x79, 0x48, 0x38, 0x4e, 0x20, 0x0a, 0xeb, 0xd5, 0x5a, 0xfe, 0xea,
0x5d, 0xce, 0x8b, 0x84, 0x67, 0x31, 0xc7, 0xeb, 0x55, 0x48, 0x85, 0xeb, 0x55, 0x84, 0x86, 0x7b,
0xb5, 0x3d, 0x26, 0xf9, 0xcb, 0xe5, 0x38, 0x99, 0xf1, 0x52, 0xb2, 0x59, 0x8e, 0xef, 0xd5, 0x00,
0x14, 0xde, 0xab, 0xb5, 0xe1, 0xd6, 0xd1, 0x90, 0x49, 0x88, 0xed, 0x27, 0x94, 0x20, 0x11, 0x78,
0x42, 0x89, 0x40, 0xe1, 0x85, 0xb5, 0x00, 0x7a, 0x93, 0xa0, 0x65, 0x25, 0x78, 0x93, 0x80, 0xa6,
0x5b, 0x07, 0x6e, 0x86, 0x19, 0x55, 0x53, 0xb3, 0xa3, 0xe9, 0x23, 0x77, 0x8a, 0x6e, 0xf4, 0x62,
0xf1, 0x13, 0xbe, 0x33, 0x9e, 0x32, 0xb5, 0x6c, 0x05, 0x8e, 0xd1, 0x1a, 0xa6, 0xcf, 0x09, 0x9f,
0xc3, 0x6a, 0x87, 0x7f, 0xb1, 0x12, 0x7d, 0x82, 0x79, 0x7c, 0x9d, 0x2b, 0xbf, 0x4f, 0xbb, 0x6d,
0xd5, 0x24, 0xf1, 0x08, 0x56, 0x58, 0x43, 0xb7, 0xe1, 0x4f, 0xa2, 0x8f, 0x1b, 0x91, 0x7d, 0x42,
0x4b, 0x37, 0xc0, 0x2f, 0xda, 0x4c, 0xfb, 0x21, 0x67, 0xdc, 0x6f, 0xf7, 0xe6, 0xed, 0x7e, 0xc8,
0x6f, 0x57, 0x09, 0xf6, 0x43, 0xc6, 0x86, 0x16, 0x13, 0xfb, 0x21, 0x04, 0xb3, 0xb3, 0xd3, 0xed,
0xde, 0xdb, 0x44, 0x5e, 0xa9, 0x7a, 0x0b, 0xcc, 0x4e, 0xaf, 0xad, 0x06, 0x22, 0x66, 0x27, 0x09,
0xc3, 0x8a, 0xa4, 0x01, 0xab, 0xb9, 0x89, 0xe5, 0x72, 0x63, 0xc8, 0x9d, 0x99, 0xeb, 0xdd, 0x20,
0x8c, 0xd7, 0x46, 0xac, 0xb7, 0x3e, 0x4f, 0x42, 0x16, 0xc0, 0xf6, 0x67, 0xa3, 0x17, 0xab, 0x1d,
0xfe, 0x59, 0xf4, 0xbd, 0x56, 0xc7, 0xf6, 0x39, 0x93, 0xf3, 0x82, 0x4f, 0x06, 0xdb, 0x1d, 0xed,
0x6e, 0x40, 0xe3, 0xfa, 0x69, 0x7f, 0x85, 0x56, 0x8d, 0xde, 0x70, 0x75, 0x58, 0x99, 0x36, 0x3c,
0x0b, 0x99, 0xf4, 0xd9, 0x60, 0x8d, 0x4e, 0xeb, 0xb4, 0xb6, 0xd9, 0x6e, 0x74, 0xed, 0x2c, 0x58,
0x92, 0xaa, 0x9b, 0xb5, 0x9f, 0x85, 0x8c, 0x7a, 0x68, 0x70, 0x9b, 0x4d, 0xaa, 0xb4, 0x32, 0xb3,
0x9a, 0xe3, 0xce, 0xf6, 0x6c, 0x93, 0xce, 0x04, 0xc8, 0xee, 0x6c, 0xab, 0x27, 0xad, 0xdd, 0xca,
0x66, 0xc9, 0xab, 0xfe, 0xec, 0x06, 0x39, 0xe6, 0x55, 0xab, 0x22, 0x91, 0xbe, 0xd5, 0x93, 0xd6,
0x5e, 0xff, 0x34, 0xfa, 0xb8, 0xed, 0x55, 0x2f, 0x44, 0xdb, 0x9d, 0xa6, 0xc0, 0x5a, 0xf4, 0xb4,
0xbf, 0x82, 0xdd, 0xd2, 0x7c, 0x95, 0x94, 0x52, 0x14, 0xcb, 0xd1, 0x95, 0xb8, 0x69, 0xde, 0x7c,
0xf0, 0x67, 0xab, 0x06, 0x86, 0x0e, 0x41, 0x6c, 0x69, 0x70, 0xb2, 0xe5, 0xca, 0xbe, 0x21, 0x51,
0x12, 0xae, 0x1c, 0xa2, 0xc3, 0x95, 0x4f, 0xda, 0x5c, 0xd5, 0xf4, 0xca, 0xbe, 0xce, 0xb1, 0x86,
0x37, 0xb5, 0xfd, 0x4a, 0xc7, 0x7a, 0x37, 0x68, 0x2b, 0x16, 0x2d, 0xde, 0x4b, 0x2e, 0x2f, 0x4d,
0x9f, 0xf0, 0x96, 0xba, 0x08, 0x51, 0xb1, 0x10, 0xa8, 0x2d, 0xba, 0xf7, 0x93, 0x94, 0xab, 0x13,
0xfd, 0xd7, 0x97, 0x97, 0xa9, 0x60, 0x13, 0x50, 0x74, 0x57, 0xe2, 0xa1, 0x2b, 0x27, 0x8a, 0x6e,
0x8c, 0xb3, 0xcf, 0x0a, 0x54, 0xd2, 0x33, 0x1e, 0x8b, 0x2c, 0x4e, 0x52, 0xf8, 0x20, 0xa8, 0xd2,
0x34, 0x42, 0xe2, 0x59, 0x81, 0x16, 0x64, 0x17, 0xc6, 0x4a, 0x54, 0x4d, 0xfb, 0xa6, 0xfd, 0x8f,
0xda, 0x8a, 0x8e, 0x98, 0x58, 0x18, 0x11, 0xcc, 0xee, 0x3d, 0x2b, 0xe1, 0x79, 0xae, 0x8c, 0xdf,
0x6b, 0x6b, 0xd5, 0x12, 0x62, 0xef, 0xe9, 0x13, 0x76, 0x0f, 0x55, 0xfd, 0x7d, 0x4f, 0xdc, 0x64,
0xca, 0xe8, 0x83, 0xb6, 0x4a, 0x23, 0x23, 0xf6, 0x50, 0x90, 0xd1, 0x86, 0x7f, 0x12, 0xfd, 0x7f,
0x65, 0xb8, 0x10, 0xf9, 0xe0, 0x0e, 0xa2, 0x50, 0x38, 0xcf, 0x6c, 0xde, 0x25, 0xe5, 0xf6, 0xd1,
0x02, 0x13, 0x1b, 0xe7, 0x25, 0x9b, 0xf2, 0xc1, 0x43, 0x62, 0xc4, 0x95, 0x94, 0x78, 0xb4, 0xa0,
0x4d, 0xf9, 0x51, 0x71, 0x22, 0x26, 0xda, 0x3a, 0xd2, 0x43, 0x23, 0x0c, 0x45, 0x85, 0x0b, 0xd9,
0x62, 0xe6, 0x84, 0x2d, 0x92, 0xa9, 0x59, 0x70, 0xea, 0xbc, 0x55, 0x82, 0x62, 0xc6, 0x32, 0x43,
0x07, 0x22, 0x8a, 0x19, 0x12, 0xd6, 0x3e, 0xff, 0x79, 0x25, 0xba, 0x67, 0x99, 0x83, 0xe6, 0xb4,
0xee, 0x30, 0xbb, 0x14, 0x55, 0xe9, 0x73, 0x94, 0x64, 0xd7, 0xe5, 0xe0, 0x0b, 0xca, 0x24, 0xce,
0x9b, 0xa6, 0x7c, 0x79, 0x6b, 0x3d, 0x5b, 0xb5, 0x36, 0x47, 0x59, 0xf6, 0x7e, 0x76, 0xad, 0x01,
0xaa, 0x56, 0x73, 0xe2, 0x05, 0x39, 0xa2, 0x6a, 0x0d, 0xf1, 0x76, 0x88, 0x8d, 0xf3, 0x54, 0x64,
0x70, 0x88, 0xad, 0x85, 0x4a, 0x48, 0x0c, 0x71, 0x0b, 0xb2, 0xf9, 0xb8, 0x11, 0xd5, 0xa7, 0x2e,
0x3b, 0x69, 0x0a, 0xf2, 0xb1, 0x51, 0x35, 0x00, 0x91, 0x8f, 0x51, 0x50, 0xfb, 0x39, 0x8b, 0xbe,
0x53, 0x5d, 0xd2, 0xd3, 0x82, 0x2f, 0x12, 0x0e, 0x1f, 0xbd, 0x70, 0x24, 0xc4, 0xfc, 0xf7, 0x09,
0x3b, 0xb3, 0xce, 0xb3, 0x32, 0x4f, 0x59, 0x79, 0xa5, 0x6f, 0xc6, 0xfb, 0x7d, 0x6e, 0x84, 0xf0,
0x76, 0xfc, 0xa3, 0x0e, 0xca, 0x26, 0xf5, 0x46, 0x66, 0x52, 0xcc, 0x2a, 0xae, 0xda, 0x4a, 0x33,
0x6b, 0x9d, 0x9c, 0x3d, 0xf1, 0x3e, 0x60, 0x69, 0xca, 0x8b, 0x65, 0x23, 0x3b, 0x66, 0x59, 0x72,
0xc9, 0x4b, 0x09, 0x4e, 0xbc, 0x35, 0x35, 0x84, 0x18, 0x71, 0xe2, 0x1d, 0xc0, 0x6d, 0x35, 0x0f,
0x3c, 0x1f, 0x66, 0x13, 0xfe, 0x0e, 0x54, 0xf3, 0xd0, 0x8e, 0x62, 0x88, 0x6a, 0x9e, 0x62, 0xed,
0xc9, 0xef, 0xcb, 0x54, 0xc4, 0xd7, 0x7a, 0x09, 0xf0, 0x07, 0x58, 0x49, 0xe0, 0x1a, 0xf0, 0x20,
0x84, 0xd8, 0x45, 0x40, 0x09, 0xce, 0x78, 0x9e, 0xb2, 0x18, 0x3e, 0x7f, 0x53, 0xeb, 0x68, 0x19,
0xb1, 0x08, 0x40, 0x06, 0x34, 0x57, 0x3f, 0xd7, 0x83, 0x35, 0x17, 0x3c, 0xd6, 0xf3, 0x20, 0x84,
0xd8, 0x65, 0x50, 0x09, 0x46, 0x79, 0x9a, 0x48, 0x30, 0x0d, 0x6a, 0x0d, 0x25, 0x21, 0xa6, 0x81,
0x4f, 0x00, 0x93, 0xc7, 0xbc, 0x98, 0x72, 0xd4, 0xa4, 0x92, 0x04, 0x4d, 0x36, 0x84, 0x7d, 0xd8,
0xb8, 0xee, 0xbb, 0xc8, 0x97, 0xe0, 0x61, 0x63, 0xdd, 0x2d, 0x91, 0x2f, 0x89, 0x87, 0x8d, 0x3d,
0x00, 0x34, 0xf1, 0x94, 0x95, 0x12, 0x6f, 0xa2, 0x92, 0x04, 0x9b, 0xd8, 0x10, 0x76, 0x8d, 0xae,
0x9b, 0x38, 0x97, 0x60, 0x8d, 0xd6, 0x0d, 0x70, 0xee, 0x40, 0xdf, 0x25, 0xe5, 0x36, 0x93, 0xd4,
0xa3, 0xc2, 0xe5, 0x7e, 0xc2, 0xd3, 0x49, 0x09, 0x32, 0x89, 0xbe, 0xee, 0x8d, 0x94, 0xc8, 0x24,
0x6d, 0x0a, 0x84, 0x92, 0x3e, 0x1f, 0xc7, 0x7a, 0x07, 0x8e, 0xc6, 0x1f, 0x84, 0x10, 0x9b, 0x9f,
0x9a, 0x46, 0xef, 0xb2, 0xa2, 0x48, 0xaa, 0xc5, 0x7f, 0x15, 0x6f, 0x50, 0x23, 0x27, 0xf2, 0x13,
0xc6, 0x81, 0xe9, 0xd5, 0x24, 0x6e, 0xac, 0x61, 0x30, 0x75, 0x7f, 0x1a, 0x64, 0x6c, 0xc5, 0xa9,
0x24, 0xce, 0x2d, 0x54, 0xec, 0x6a, 0x22, 0x77, 0x50, 0x57, 0xbb, 0x30, 0xe7, 0x65, 0x20, 0xe3,
0xe2, 0x58, 0x2c, 0xf8, 0x58, 0xbc, 0x7a, 0x97, 0x94, 0x32, 0xc9, 0xa6, 0x7a, 0xe5, 0x7e, 0x4e,
0x58, 0xc2, 0x60, 0xe2, 0x65, 0xa0, 0x4e, 0x25, 0x5b, 0x40, 0x80, 0xb6, 0x9c, 0xf0, 0x1b, 0xb4,
0x80, 0x80, 0x16, 0x0d, 0x47, 0x14, 0x10, 0x21, 0xde, 0x9e, 0xa3, 0x18, 0xe7, 0xfa, 0x8d, 0xe9,
0xb1, 0x68, 0x6a, 0x39, 0xca, 0x1a, 0x04, 0x89, 0xad, 0x6c, 0x50, 0xc1, 0xee, 0x2f, 0x8d, 0x7f,
0x3b, 0xc5, 0xd6, 0x09, 0x3b, 0xed, 0x69, 0xf6, 0xb8, 0x07, 0x89, 0xb8, 0xb2, 0xcf, 0x01, 0x50,
0xae, 0xda, 0x8f, 0x01, 0x3c, 0xee, 0x41, 0x3a, 0x67, 0x32, 0x6e, 0xb7, 0x5e, 0xb2, 0xf8, 0x7a,
0x5a, 0x88, 0x79, 0x36, 0xd9, 0x15, 0xa9, 0x28, 0xc0, 0x99, 0x8c, 0xd7, 0x6a, 0x80, 0x12, 0x67,
0x32, 0x1d, 0x2a, 0xb6, 0x82, 0x73, 0x5b, 0xb1, 0x93, 0x26, 0x53, 0xb8, 0xa3, 0xf6, 0x0c, 0x29,
0x80, 0xa8, 0xe0, 0x50, 0x10, 0x09, 0xa2, 0x7a, 0xc7, 0x2d, 0x93, 0x98, 0xa5, 0xb5, 0xbf, 0x6d,
0xda, 0x8c, 0x07, 0x76, 0x06, 0x11, 0xa2, 0x80, 0xf4, 0x73, 0x3c, 0x2f, 0xb2, 0xc3, 0x4c, 0x0a,
0xb2, 0x9f, 0x0d, 0xd0, 0xd9, 0x4f, 0x07, 0x04, 0x69, 0x75, 0xcc, 0xdf, 0x55, 0xad, 0xa9, 0xfe,
0xc1, 0xd2, 0x6a, 0xf5, 0xf7, 0xa1, 0x96, 0x87, 0xd2, 0x2a, 0xe0, 0x40, 0x67, 0xb4, 0x93, 0x3a,
0x60, 0x02, 0xda, 0x7e, 0x98, 0xac, 0x77, 0x83, 0xb8, 0x9f, 0x91, 0x5c, 0xa6, 0x3c, 0xe4, 0x47,
0x01, 0x7d, 0xfc, 0x34, 0xa0, 0x3d, 0x6e, 0xf1, 0xfa, 0x73, 0xc5, 0xe3, 0xeb, 0xd6, 0x63, 0x4d,
0x7e, 0x43, 0x6b, 0x84, 0x38, 0x6e, 0x21, 0x50, 0x7c, 0x88, 0x0e, 0x63, 0x91, 0x85, 0x86, 0xa8,
0x92, 0xf7, 0x19, 0x22, 0xcd, 0xd9, 0xcd, 0xaf, 0x91, 0xea, 0xc8, 0xac, 0x87, 0x69, 0x83, 0xb0,
0xe0, 0x42, 0xc4, 0xe6, 0x97, 0x84, 0x6d, 0x4d, 0x0e, 0x7d, 0x1e, 0xb7, 0x9f, 0xf9, 0x6e, 0x59,
0x39, 0xa6, 0x9f, 0xf9, 0xa6, 0x58, 0xba, 0x93, 0x75, 0x8c, 0x74, 0x58, 0xf1, 0xe3, 0x64, 0xb3,
0x1f, 0x6c, 0xb7, 0x3c, 0x9e, 0xcf, 0xdd, 0x94, 0xb3, 0xa2, 0xf6, 0xba, 0x15, 0x30, 0x64, 0x31,
0x62, 0xcb, 0x13, 0xc0, 0x41, 0x0a, 0xf3, 0x3c, 0xef, 0x8a, 0x4c, 0xf2, 0x4c, 0x62, 0x29, 0xcc,
0x37, 0xa6, 0xc1, 0x50, 0x0a, 0xa3, 0x14, 0x40, 0xdc, 0xaa, 0xf3, 0x20, 0x2e, 0x4f, 0xd8, 0x0c,
0xad, 0xd8, 0xea, 0xb3, 0x9e, 0x5a, 0x1e, 0x8a, 0x5b, 0xc0, 0x39, 0x37, 0xf9, 0x5c, 0x2f, 0x63,
0x56, 0x4c, 0xcd, 0xe9, 0xc6, 0x64, 0xf0, 0x94, 0xb6, 0xe3, 0x93, 0xc4, 0x4d, 0xbe, 0xb0, 0x06,
0x48, 0x3b, 0x87, 0x33, 0x36, 0x35, 0x3d, 0x45, 0x7a, 0xa0, 0xe4, 0xad, 0xae, 0xae, 0x77, 0x83,
0xc0, 0xcf, 0x9b, 0x64, 0xc2, 0x45, 0xc0, 0x8f, 0x92, 0xf7, 0xf1, 0x03, 0x41, 0x50, 0xbd, 0x55,
0xfd, 0xae, 0x77, 0x74, 0x3b, 0xd9, 0x44, 0xef, 0x63, 0x87, 0xc4, 0xe5, 0x01, 0x5c, 0xa8, 0x7a,
0x23, 0x78, 0x30, 0x47, 0x9b, 0x03, 0xda, 0xd0, 0x1c, 0x35, 0xe7, 0xaf, 0x7d, 0xe6, 0x28, 0x06,
0x6b, 0x9f, 0x3f, 0xd3, 0x73, 0x74, 0x8f, 0x49, 0x56, 0xd5, 0xed, 0x6f, 0x12, 0x7e, 0xa3, 0x37,
0xc2, 0x48, 0x7f, 0x1b, 0x6a, 0xa8, 0x5e, 0x59, 0x05, 0xbb, 0xe2, 0xed, 0xde, 0x7c, 0xc0, 0xb7,
0xde, 0x21, 0x74, 0xfa, 0x06, 0x5b, 0x85, 0xed, 0xde, 0x7c, 0xc0, 0xb7, 0x7e, 0x17, 0xbe, 0xd3,
0x37, 0x78, 0x21, 0x7e, 0xbb, 0x37, 0xaf, 0x7d, 0xff, 0x65, 0x33, 0x71, 0x5d, 0xe7, 0x55, 0x1d,
0x16, 0xcb, 0x64, 0xc1, 0xb1, 0x72, 0xd2, 0xb7, 0x67, 0xd0, 0x50, 0x39, 0x49, 0xab, 0x38, 0x1f,
0x50, 0xc2, 0x5a, 0x71, 0x2a, 0xca, 0x44, 0xdd, 0xa4, 0x7f, 0xde, 0xc3, 0x68, 0x03, 0x87, 0x36,
0x4d, 0x21, 0x25, 0x7b, 0xbb, 0xd1, 0x43, 0xed, 0x53, 0xcc, 0x9b, 0x01, 0x7b, 0xed, 0x87, 0x99,
0xb7, 0x7a, 0xd2, 0xf6, 0xc6, 0x9f, 0xc7, 0xb8, 0x77, 0x1c, 0x43, 0xa3, 0x8a, 0xde, 0x74, 0x7c,
0xda, 0x5f, 0x41, 0xbb, 0xff, 0xeb, 0x66, 0x5f, 0x01, 0xfd, 0xeb, 0x49, 0xf0, 0xac, 0x8f, 0x45,
0x30, 0x11, 0x9e, 0xdf, 0x4a, 0x47, 0x37, 0xe4, 0xef, 0x9b, 0x0d, 0x74, 0x83, 0xaa, 0x77, 0x39,
0xd4, 0x3b, 0xa0, 0x7a, 0x4e, 0x84, 0x86, 0xd5, 0xc2, 0x70, 0x66, 0xbc, 0xb8, 0xa5, 0x96, 0xf3,
0x39, 0x2d, 0x0f, 0xd6, 0xef, 0x1c, 0x3a, 0xed, 0x09, 0x59, 0x76, 0x68, 0xd8, 0xa0, 0x2f, 0x6e,
0xab, 0x46, 0xcd, 0x15, 0x07, 0x56, 0x5f, 0xe7, 0x78, 0xde, 0xd3, 0xb0, 0xf7, 0xbd, 0x8e, 0xcf,
0x6f, 0xa7, 0xa4, 0xdb, 0xf2, 0x1f, 0x2b, 0xd1, 0x23, 0x8f, 0xb5, 0xf7, 0x13, 0xc0, 0xa9, 0xc7,
0x8f, 0x03, 0xf6, 0x29, 0x25, 0xd3, 0xb8, 0xdf, 0xfd, 0xe5, 0x94, 0xed, 0xb7, 0xa7, 0x3c, 0x95,
0xfd, 0x24, 0x95, 0xbc, 0x68, 0x7f, 0x7b, 0xca, 0xb7, 0x5b, 0x53, 0x43, 0xfa, 0xdb, 0x53, 0x01,
0xdc, 0xf9, 0xf6, 0x14, 0xe2, 0x19, 0xfd, 0xf6, 0x14, 0x6a, 0x2d, 0xf8, 0xed, 0xa9, 0xb0, 0x06,
0x95, 0xde, 0x9b, 0x26, 0xd4, 0xe7, 0xd6, 0xbd, 0x2c, 0xfa, 0xc7, 0xd8, 0xcf, 0x6e, 0xa3, 0x42,
0x2c, 0x70, 0x35, 0xa7, 0x9e, 0x73, 0xeb, 0x71, 0x4d, 0xbd, 0x67, 0xdd, 0xb6, 0x7b, 0xf3, 0xda,
0xf7, 0x4f, 0xf5, 0xee, 0xc6, 0xa4, 0x73, 0x51, 0xa8, 0xef, 0x8e, 0x6d, 0x84, 0xd2, 0x73, 0x65,
0xc1, 0x1d, 0xf9, 0xcd, 0x7e, 0x30, 0xd1, 0xdd, 0x8a, 0xd0, 0x83, 0x3e, 0xec, 0x32, 0x04, 0x86,
0x7c, 0xbb, 0x37, 0x4f, 0x2c, 0x23, 0xb5, 0xef, 0x7a, 0xb4, 0x7b, 0x18, 0xf3, 0xc7, 0xfa, 0x69,
0x7f, 0x05, 0xed, 0x7e, 0xa1, 0xcb, 0x46, 0xd7, 0xbd, 0x1a, 0xe7, 0xad, 0x2e, 0x53, 0x23, 0x6f,
0x98, 0x87, 0x7d, 0xf1, 0x50, 0x01, 0xe1, 0x2e, 0xa1, 0x5d, 0x05, 0x04, 0xba, 0x8c, 0x7e, 0x7e,
0x3b, 0x25, 0xdd, 0x96, 0x7f, 0x5a, 0x89, 0xee, 0x92, 0x6d, 0xd1, 0x71, 0xf0, 0x45, 0x5f, 0xcb,
0x20, 0x1e, 0xbe, 0xbc, 0xb5, 0x9e, 0x6e, 0xd4, 0xbf, 0xae, 0x44, 0xf7, 0x02, 0x8d, 0xaa, 0x03,
0xe4, 0x16, 0xd6, 0xfd, 0x40, 0xf9, 0xe1, 0xed, 0x15, 0xa9, 0xe5, 0xde, 0xc5, 0x47, 0xed, 0x8f,
0x32, 0x05, 0x6c, 0x8f, 0xe8, 0x8f, 0x32, 0x75, 0x6b, 0xc1, 0x43, 0x1e, 0x76, 0xd1, 0x6c, 0xba,
0xd0, 0x43, 0x1e, 0xf5, 0x84, 0x5a, 0xf0, 0xe3, 0x12, 0x18, 0x87, 0x39, 0x79, 0xf5, 0x2e, 0x67,
0xd9, 0x84, 0x76, 0x52, 0xcb, 0xbb, 0x9d, 0x18, 0x0e, 0x1e, 0x8e, 0x55, 0xd2, 0x33, 0xd1, 0x6c,
0xa4, 0x1e, 0x53, 0xfa, 0x06, 0x09, 0x1e, 0x8e, 0xb5, 0x50, 0xc2, 0x9b, 0xae, 0x1a, 0x43, 0xde,
0x40, 0xb1, 0xf8, 0xa4, 0x0f, 0x0a, 0x4a, 0x74, 0xe3, 0xcd, 0x9c, 0xb9, 0x6f, 0x86, 0xac, 0xb4,
0xce, 0xdd, 0xb7, 0x7a, 0xd2, 0x84, 0xdb, 0x11, 0x97, 0x5f, 0x71, 0x36, 0xe1, 0x45, 0xd0, 0xad,
0xa1, 0x7a, 0xb9, 0x75, 0x69, 0xcc, 0xed, 0xae, 0x48, 0xe7, 0xb3, 0x4c, 0x0f, 0x26, 0xe9, 0xd6,
0xa5, 0xba, 0xdd, 0x02, 0x1a, 0x1e, 0x0b, 0x5a, 0xb7, 0xaa, 0xbc, 0x7c, 0x12, 0x36, 0xe3, 0x55,
0x95, 0x1b, 0xbd, 0x58, 0xba, 0x9f, 0x3a, 0x8c, 0x3a, 0xfa, 0x09, 0x22, 0x69, 0xab, 0x27, 0x0d,
0xcf, 0xe7, 0x1c, 0xb7, 0x26, 0x9e, 0xb6, 0x3b, 0x6c, 0xb5, 0x42, 0xea, 0x69, 0x7f, 0x05, 0x78,
0x1a, 0xaa, 0xa3, 0xea, 0x28, 0x29, 0xe5, 0x7e, 0x92, 0xa6, 0x83, 0x8d, 0x40, 0x98, 0x34, 0x50,
0xf0, 0x34, 0x14, 0x81, 0x89, 0x48, 0x6e, 0x4e, 0x0f, 0xb3, 0x41, 0x97, 0x1d, 0x45, 0xf5, 0x8a,
0x64, 0x97, 0x06, 0x27, 0x5a, 0xce, 0xa5, 0x36, 0xbd, 0x1d, 0x86, 0x2f, 0x5c, 0xab, 0xc3, 0xdb,
0xbd, 0x79, 0x70, 0xbb, 0x5d, 0x51, 0x6a, 0x65, 0x79, 0x48, 0x99, 0xf0, 0x56, 0x92, 0x47, 0x1d,
0x14, 0x38, 0x15, 0xac, 0xa7, 0xd1, 0xdb, 0x64, 0x32, 0xe5, 0x12, 0xbd, 0x53, 0xe4, 0x02, 0xc1,
0x3b, 0x45, 0x00, 0x04, 0x43, 0x57, 0xff, 0xdd, 0x1c, 0x87, 0x1e, 0x4e, 0xb0, 0xa1, 0xd3, 0xca,
0x0e, 0x15, 0x1a, 0x3a, 0x94, 0x06, 0xd9, 0xc0, 0xb8, 0xd5, 0xaf, 0xe3, 0x3f, 0x09, 0x99, 0x01,
0xef, 0xe4, 0x6f, 0xf4, 0x62, 0xc1, 0x8a, 0x62, 0x1d, 0x26, 0xb3, 0x44, 0x62, 0x2b, 0x8a, 0x63,
0xa3, 0x42, 0x42, 0x2b, 0x4a, 0x1b, 0xa5, 0xba, 0x57, 0xd5, 0x08, 0x87, 0x93, 0x70, 0xf7, 0x6a,
0xa6, 0x5f, 0xf7, 0x0c, 0xdb, 0xba, 0xb1, 0x99, 0x99, 0x90, 0x91, 0x57, 0x7a, 0xb3, 0x8c, 0xc4,
0xb6, 0x7a, 0x4d, 0x13, 0x82, 0xa1, 0xac, 0x43, 0x29, 0xc0, 0x03, 0xfb, 0x8a, 0x6b, 0xee, 0xbd,
0xe6, 0x39, 0x67, 0x05, 0xcb, 0x62, 0x74, 0x73, 0xaa, 0x0c, 0xb6, 0xc8, 0xd0, 0xe6, 0x94, 0xd4,
0x00, 0xb7, 0xcd, 0xfd, 0x17, 0x2c, 0x91, 0xa9, 0x60, 0xde, 0x64, 0xf4, 0xdf, 0xaf, 0x7c, 0xdc,
0x83, 0x84, 0xb7, 0xcd, 0x1b, 0xc0, 0x1c, 0x7c, 0xd7, 0x4e, 0x3f, 0x0b, 0x98, 0xf2, 0xd1, 0xd0,
0x46, 0x98, 0x56, 0x01, 0x41, 0x6d, 0x0a, 0x5c, 0x2e, 0x7f, 0xc2, 0x97, 0x58, 0x50, 0xdb, 0xfa,
0x54, 0x21, 0xa1, 0xa0, 0x6e, 0xa3, 0xa0, 0xce, 0x74, 0xf7, 0x41, 0xab, 0x01, 0x7d, 0x77, 0xeb,
0xb3, 0xd6, 0xc9, 0x81, 0x99, 0xb3, 0x97, 0x2c, 0xbc, 0xfb, 0x04, 0x48, 0x43, 0xf7, 0x92, 0x05,
0x7e, 0x9b, 0x60, 0xa3, 0x17, 0x0b, 0x6f, 0xc9, 0x33, 0xc9, 0xdf, 0x35, 0xf7, 0xca, 0x91, 0xe6,
0x2a, 0x79, 0xeb, 0x66, 0xf9, 0x7a, 0x37, 0x68, 0x1f, 0x80, 0x3d, 0x2d, 0x44, 0xcc, 0xcb, 0x52,
0x7f, 0xa9, 0xd2, 0x7f, 0xc2, 0x48, 0xcb, 0x86, 0xe0, 0x3b, 0x95, 0x0f, 0xc3, 0x90, 0xf3, 0x79,
0xb9, 0x5a, 0x64, 0xbf, 0x7a, 0xb3, 0x8a, 0x6a, 0xb6, 0x3f, 0x78, 0xb3, 0xd6, 0xc9, 0xd9, 0xe9,
0xa5, 0xa5, 0xee, 0x67, 0x6e, 0xd6, 0x51, 0x75, 0xec, 0x0b, 0x37, 0x8f, 0x7b, 0x90, 0xda, 0xd5,
0x57, 0xd1, 0xfb, 0x47, 0x62, 0x3a, 0xe2, 0xd9, 0x64, 0xf0, 0x03, 0xff, 0x11, 0x5a, 0x31, 0x1d,
0x56, 0x7f, 0x36, 0x46, 0xef, 0x50, 0x62, 0xfb, 0x10, 0xe0, 0x1e, 0xbf, 0x98, 0x4f, 0x47, 0x92,
0x49, 0xf0, 0x10, 0xa0, 0xfa, 0xfb, 0xb0, 0x12, 0x10, 0x0f, 0x01, 0x7a, 0x00, 0xb0, 0x37, 0x2e,
0x38, 0x47, 0xed, 0x55, 0x82, 0xa0, 0x3d, 0x0d, 0xd8, 0x2a, 0xc2, 0xd8, 0xab, 0x0a, 0x75, 0xf8,
0xd0, 0x9e, 0xd5, 0x51, 0x52, 0xa2, 0x8a, 0x68, 0x53, 0x36, 0xb8, 0xeb, 0xee, 0xab, 0xaf, 0x8e,
0xcc, 0x67, 0x33, 0x56, 0x2c, 0x41, 0x70, 0xeb, 0x5e, 0x3a, 0x00, 0x11, 0xdc, 0x28, 0x68, 0x67,
0x6d, 0x73, 0x99, 0xe3, 0xeb, 0x03, 0x51, 0x88, 0xb9, 0x4c, 0x32, 0x0e, 0xbf, 0x3c, 0x61, 0x2e,
0xa8, 0xcb, 0x10, 0xb3, 0x96, 0x62, 0x6d, 0x95, 0xab, 0x88, 0xfa, 0x79, 0x42, 0xf5, 0xfd, 0xea,
0x52, 0x8a, 0x02, 0xde, 0x4f, 0xac, 0xad, 0x40, 0x88, 0xa8, 0x72, 0x49, 0x18, 0x8c, 0xfd, 0x69,
0x92, 0x4d, 0xd1, 0xb1, 0x3f, 0x75, 0xbf, 0xfe, 0x7a, 0x8f, 0x06, 0xec, 0x84, 0xaa, 0x2f, 0x5a,
0x3d, 0x01, 0xf4, 0xbb, 0x9c, 0xe8, 0x45, 0x77, 0x09, 0x62, 0x42, 0xe1, 0x24, 0x70, 0xf5, 0x3a,
0xe7, 0x19, 0x9f, 0x34, 0x4f, 0xcd, 0x61, 0xae, 0x3c, 0x22, 0xe8, 0x0a, 0x92, 0x36, 0x17, 0x29,
0xf9, 0xd9, 0x3c, 0x3b, 0x2d, 0xc4, 0x65, 0x92, 0xf2, 0x02, 0xe4, 0xa2, 0x5a, 0xdd, 0x91, 0x13,
0xb9, 0x08, 0xe3, 0xec, 0xe3, 0x17, 0x4a, 0xea, 0x7d, 0x84, 0x7d, 0x5c, 0xb0, 0x18, 0x3e, 0x7e,
0x51, 0xdb, 0x68, 0x63, 0xc4, 0xc9, 0x60, 0x00, 0x77, 0x0a, 0x9d, 0xda, 0x75, 0xb6, 0x54, 0xf1,
0xa1, 0xdf, 0x25, 0x54, 0xdf, 0x44, 0x2d, 0x41, 0xa1, 0xa3, 0xcd, 0x61, 0x24, 0x51, 0xe8, 0x84,
0x35, 0xec, 0x52, 0xa2, 0xb8, 0x13, 0xfd, 0x58, 0x11, 0x58, 0x4a, 0x6a, 0x1b, 0x8d, 0x90, 0x58,
0x4a, 0x5a, 0x10, 0x48, 0x48, 0xcd, 0x34, 0x98, 0xa2, 0x09, 0xc9, 0x48, 0x83, 0x09, 0xc9, 0xa5,
0x6c, 0xa2, 0x38, 0xcc, 0x12, 0x99, 0xb0, 0x74, 0xc4, 0xe5, 0x29, 0x2b, 0xd8, 0x8c, 0x4b, 0x5e,
0xc0, 0x44, 0xa1, 0x91, 0xa1, 0xc7, 0x10, 0x89, 0x82, 0x62, 0xb5, 0xc3, 0xdf, 0x8b, 0x3e, 0xac,
0xd6, 0x7d, 0x9e, 0xe9, 0x9f, 0x5b, 0x79, 0xa5, 0x7e, 0xa7, 0x69, 0xf0, 0x91, 0xb1, 0x31, 0x92,
0x05, 0x67, 0xb3, 0xc6, 0xf6, 0x07, 0xe6, 0xef, 0x0a, 0x7c, 0xba, 0x52, 0xc5, 0xf3, 0x89, 0x90,
0xc9, 0x65, 0xb5, 0xcd, 0xd6, 0x6f, 0x10, 0x81, 0x78, 0x76, 0xc5, 0xc3, 0xc0, 0xb7, 0x28, 0x30,
0xce, 0xe6, 0x69, 0x57, 0x7a, 0xc6, 0xf3, 0x14, 0xe6, 0x69, 0x4f, 0x5b, 0x01, 0x44, 0x9e, 0x46,
0x41, 0x3b, 0x39, 0x5d, 0xf1, 0x98, 0x87, 0x3b, 0x33, 0xe6, 0xfd, 0x3a, 0x33, 0xf6, 0x5e, 0xca,
0x48, 0xa3, 0x0f, 0x8f, 0xf9, 0xec, 0x82, 0x17, 0xe5, 0x55, 0x92, 0x53, 0xdf, 0x6d, 0xb5, 0x44,
0xe7, 0x77, 0x5b, 0x09, 0xd4, 0xae, 0x04, 0x16, 0x38, 0x2c, 0x4f, 0xd8, 0x8c, 0xab, 0x2f, 0x6b,
0x80, 0x95, 0xc0, 0x31, 0xe2, 0x40, 0xc4, 0x4a, 0x40, 0xc2, 0xce, 0xfb, 0x5d, 0x96, 0x39, 0xe3,
0xd3, 0x2a, 0xc2, 0x8a, 0x53, 0xb6, 0x9c, 0xf1, 0x4c, 0x6a, 0x93, 0xe0, 0x4c, 0xde, 0x31, 0x89,
0xf3, 0xc4, 0x99, 0x7c, 0x1f, 0x3d, 0x27, 0x35, 0x79, 0x17, 0xfe, 0x54, 0x14, 0xb2, 0xfe, 0x31,
0xa5, 0xf3, 0x22, 0x05, 0xa9, 0xc9, 0xbf, 0xa8, 0x1e, 0x49, 0xa4, 0xa6, 0xb0, 0x86, 0xf3, 0x2b,
0x04, 0x5e, 0x1b, 0xde, 0xf0, 0xc2, 0xc4, 0xc9, 0xab, 0x19, 0x4b, 0x52, 0x1d, 0x0d, 0x3f, 0x0a,
0xd8, 0x26, 0x74, 0x88, 0x5f, 0x21, 0xe8, 0xab, 0xeb, 0xfc, 0x6e, 0x43, 0xb8, 0x85, 0xe0, 0x16,
0x41, 0x87, 0x7d, 0xe2, 0x16, 0x41, 0xb7, 0x96, 0xdd, 0xb9, 0x5b, 0x56, 0x71, 0x4b, 0x45, 0xec,
0x8a, 0x09, 0x3c, 0x2f, 0x74, 0x6c, 0x02, 0x90, 0xd8, 0xb9, 0x07, 0x15, 0x6c, 0x69, 0x60, 0xb1,
0xfd, 0x24, 0x63, 0x69, 0xf2, 0x33, 0x58, 0xd6, 0x3b, 0x76, 0x1a, 0x82, 0x28, 0x0d, 0x70, 0x12,
0x73, 0x75, 0xc0, 0xe5, 0x38, 0xa9, 0x52, 0xff, 0x7a, 0xe0, 0xba, 0x29, 0xa2, 0xdb, 0x95, 0x43,
0x3a, 0xdf, 0x68, 0x85, 0x97, 0x75, 0x27, 0xcf, 0x47, 0xd5, 0xaa, 0x7a, 0xc6, 0x63, 0x9e, 0xe4,
0x72, 0xf0, 0x22, 0x7c, 0xad, 0x00, 0x4e, 0x3c, 0x68, 0xd1, 0x43, 0xcd, 0xb9, 0x7d, 0x5f, 0xe5,
0x92, 0x51, 0xfd, 0x2b, 0x83, 0xe7, 0x25, 0x2f, 0x74, 0xa1, 0x71, 0xc0, 0x25, 0x98, 0x9d, 0x0e,
0x37, 0x74, 0xc0, 0xaa, 0xa3, 0xc4, 0xec, 0x0c, 0x6b, 0xd8, 0xc3, 0x3e, 0x87, 0xd3, 0xdf, 0xdc,
0x56, 0xcf, 0x1b, 0x6e, 0x92, 0xc6, 0x1c, 0x8a, 0x38, 0xec, 0xa3, 0x69, 0x5b, 0xad, 0xb5, 0xdd,
0xee, 0x64, 0xcb, 0x43, 0xf8, 0xc8, 0x04, 0x62, 0x49, 0x61, 0x44, 0xb5, 0x16, 0xc0, 0x9d, 0xc3,
0xf0, 0x42, 0xb0, 0x49, 0xcc, 0x4a, 0x79, 0xca, 0x96, 0xa9, 0x60, 0x13, 0xb5, 0xae, 0xc3, 0xc3,
0xf0, 0x86, 0x19, 0xba, 0x10, 0x75, 0x18, 0x4e, 0xc1, 0x6e, 0x75, 0xa6, 0x7e, 0x3c, 0x51, 0x3f,
0xcb, 0x09, 0xab, 0x33, 0xd5, 0x5e, 0xf8, 0x1c, 0xe7, 0xc3, 0x30, 0x64, 0xdf, 0x41, 0xab, 0x45,
0xaa, 0x0c, 0xb9, 0x87, 0xe9, 0x78, 0x05, 0xc8, 0xfd, 0x00, 0x61, 0xbf, 0x4b, 0x51, 0xff, 0xbd,
0xf9, 0xfd, 0x1f, 0xa9, 0xbf, 0x64, 0xbd, 0x89, 0xe9, 0xba, 0xd0, 0xd0, 0xfd, 0xc0, 0xdd, 0x56,
0x4f, 0xda, 0x96, 0x99, 0xbb, 0x57, 0x4c, 0xee, 0x4c, 0x26, 0xc7, 0xbc, 0x44, 0x5e, 0x28, 0xaf,
0x84, 0x43, 0x2b, 0x25, 0xca, 0xcc, 0x36, 0x65, 0x03, 0xbd, 0x92, 0xbd, 0x9a, 0x24, 0x52, 0xcb,
0x9a, 0x27, 0xa4, 0x37, 0xdb, 0x06, 0xda, 0x14, 0xd1, 0x2b, 0x9a, 0xb6, 0xb9, 0xbc, 0x62, 0xc6,
0x62, 0x3a, 0x4d, 0xb9, 0x86, 0xce, 0x38, 0xab, 0x3f, 0xe4, 0xb7, 0xdd, 0xb6, 0x85, 0x82, 0x44,
0x2e, 0x0f, 0x2a, 0xd8, 0x32, 0xb2, 0xc2, 0xea, 0x5b, 0x52, 0xcd, 0x85, 0x5d, 0x6b, 0x9b, 0xf1,
0x00, 0xa2, 0x8c, 0x44, 0x41, 0xfb, 0xde, 0x5b, 0x25, 0x3e, 0xe0, 0xcd, 0x95, 0x80, 0x9f, 0x20,
0x52, 0xca, 0x8e, 0x98, 0x78, 0xef, 0x0d, 0xc1, 0xec, 0x3e, 0x01, 0x78, 0x78, 0xb9, 0x3c, 0x9c,
0xc0, 0x7d, 0x02, 0xd4, 0x57, 0x0c, 0xb1, 0x4f, 0xa0, 0x58, 0x7f, 0xe8, 0xcc, 0xb9, 0xd7, 0x11,
0x2b, 0x6d, 0xe7, 0x90, 0xa1, 0x43, 0xc1, 0xd0, 0xd0, 0x51, 0x0a, 0xfe, 0x25, 0x75, 0x8f, 0xd6,
0x90, 0x4b, 0x8a, 0x9d, 0xab, 0xad, 0x76, 0x61, 0x4e, 0xe1, 0xe3, 0x75, 0x71, 0x2c, 0x74, 0x33,
0xf4, 0x7b, 0x8d, 0x25, 0x28, 0x7c, 0xfc, 0x66, 0xb7, 0x68, 0xa2, 0xf0, 0xe9, 0xd6, 0xb2, 0x79,
0xd2, 0xec, 0x6f, 0xd5, 0x23, 0x54, 0xf8, 0x2f, 0x0a, 0xd4, 0x42, 0x22, 0x4f, 0xb6, 0xa0, 0xda,
0xf6, 0xcb, 0xfb, 0xff, 0xf9, 0xcd, 0x9d, 0x95, 0x5f, 0x7c, 0x73, 0x67, 0xe5, 0xbf, 0xbf, 0xb9,
0xb3, 0xf2, 0xf3, 0x6f, 0xef, 0xbc, 0xf7, 0x8b, 0x6f, 0xef, 0xbc, 0xf7, 0x5f, 0xdf, 0xde, 0x79,
0xef, 0xeb, 0xf7, 0xf5, 0x8f, 0xfc, 0x5e, 0xfc, 0x3f, 0xf5, 0x53, 0xbd, 0xcf, 0xff, 0x2f, 0x00,
0x00, 0xff, 0xff, 0x42, 0x38, 0x0e, 0xf7, 0x08, 0x78, 0x00, 0x00,
// 5445 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x9d, 0xdd, 0x6f, 0x24, 0x49,
0x52, 0xc0, 0xcf, 0x2f, 0x2c, 0xd4, 0x71, 0x0b, 0xf4, 0xc2, 0xb2, 0xb7, 0xdc, 0xcd, 0xcc, 0xce,
0xce, 0xd8, 0x33, 0x63, 0xbb, 0x3d, 0x3b, 0xb3, 0x1f, 0xa7, 0x3b, 0x24, 0xd4, 0x63, 0x8f, 0xbd,
0xbe, 0xb3, 0x3d, 0xc6, 0xdd, 0x9e, 0x91, 0x56, 0x42, 0xa2, 0xdc, 0x95, 0x6e, 0x17, 0xae, 0xae,
0xac, 0xab, 0xca, 0x6e, 0x4f, 0x1f, 0x02, 0x81, 0x40, 0x20, 0x10, 0x88, 0x13, 0x5f, 0xaf, 0x48,
0xfc, 0x35, 0x88, 0xa7, 0x7b, 0xe4, 0x11, 0xed, 0xfe, 0x05, 0xfc, 0x07, 0xa8, 0x32, 0xb3, 0xf2,
0x23, 0x2a, 0x22, 0xab, 0x7c, 0x4f, 0x33, 0x72, 0xfc, 0x22, 0x22, 0x3f, 0x23, 0x23, 0xb3, 0xb2,
0xaa, 0xa3, 0xbb, 0xc5, 0xc5, 0x4e, 0x51, 0x72, 0xc1, 0xab, 0x9d, 0x8a, 0x95, 0xcb, 0x74, 0xca,
0x9a, 0x7f, 0x87, 0xf2, 0xcf, 0x83, 0x77, 0xe2, 0x7c, 0x25, 0x56, 0x05, 0xfb, 0xf0, 0x03, 0x4b,
0x4e, 0xf9, 0x7c, 0x1e, 0xe7, 0x49, 0xa5, 0x90, 0x0f, 0xdf, 0xb7, 0x12, 0xb6, 0x64, 0xb9, 0xd0,
0x7f, 0x7f, 0xf6, 0xdf, 0xff, 0xb7, 0x16, 0xbd, 0xbb, 0x9b, 0xa5, 0x2c, 0x17, 0xbb, 0x5a, 0x63,
0xf0, 0x55, 0xf4, 0x9d, 0x51, 0x51, 0x1c, 0x30, 0xf1, 0x9a, 0x95, 0x55, 0xca, 0xf3, 0xc1, 0xc7,
0x43, 0xed, 0x60, 0x78, 0x56, 0x4c, 0x87, 0xa3, 0xa2, 0x18, 0x5a, 0xe1, 0xf0, 0x8c, 0xfd, 0x74,
0xc1, 0x2a, 0xf1, 0xe1, 0x83, 0x30, 0x54, 0x15, 0x3c, 0xaf, 0xd8, 0xe0, 0x32, 0xfa, 0xad, 0x51,
0x51, 0x8c, 0x99, 0xd8, 0x63, 0x75, 0x05, 0xc6, 0x22, 0x16, 0x6c, 0xb0, 0xd1, 0x52, 0xf5, 0x01,
0xe3, 0xe3, 0x51, 0x37, 0xa8, 0xfd, 0x4c, 0xa2, 0x6f, 0xd7, 0x7e, 0xae, 0x16, 0x22, 0xe1, 0x37,
0xf9, 0xe0, 0xa3, 0xb6, 0xa2, 0x16, 0x19, 0xdb, 0xf7, 0x43, 0x88, 0xb6, 0xfa, 0x26, 0xfa, 0xf5,
0x37, 0x71, 0x96, 0x31, 0xb1, 0x5b, 0xb2, 0xba, 0xe0, 0xbe, 0x8e, 0x12, 0x0d, 0x95, 0xcc, 0xd8,
0xfd, 0x38, 0xc8, 0x68, 0xc3, 0x5f, 0x45, 0xdf, 0x51, 0x92, 0x33, 0x36, 0xe5, 0x4b, 0x56, 0x0e,
0x50, 0x2d, 0x2d, 0x24, 0x9a, 0xbc, 0x05, 0x41, 0xdb, 0xbb, 0x3c, 0x5f, 0xb2, 0x52, 0xe0, 0xb6,
0xb5, 0x30, 0x6c, 0xdb, 0x42, 0xda, 0xf6, 0xdf, 0xad, 0x45, 0xdf, 0x1b, 0x4d, 0xa7, 0x7c, 0x91,
0x8b, 0x23, 0x3e, 0x8d, 0xb3, 0xa3, 0x34, 0xbf, 0x3e, 0x61, 0x37, 0xbb, 0x57, 0x35, 0x9f, 0xcf,
0xd8, 0xe0, 0xb9, 0xdf, 0xaa, 0x0a, 0x1d, 0x1a, 0x76, 0xe8, 0xc2, 0xc6, 0xf7, 0xa7, 0xb7, 0x53,
0xd2, 0x65, 0xf9, 0xa7, 0xb5, 0xe8, 0x0e, 0x2c, 0xcb, 0x98, 0x67, 0x4b, 0x66, 0x4b, 0xf3, 0x59,
0x87, 0x61, 0x1f, 0x37, 0xe5, 0xf9, 0xfc, 0xb6, 0x6a, 0xba, 0x44, 0x59, 0xf4, 0x9e, 0x3b, 0x5c,
0xc6, 0xac, 0x92, 0xd3, 0xe9, 0x31, 0x3d, 0x22, 0x34, 0x62, 0x3c, 0x3f, 0xe9, 0x83, 0x6a, 0x6f,
0x69, 0x34, 0xd0, 0xde, 0x32, 0x5e, 0x19, 0x67, 0x8f, 0x50, 0x0b, 0x0e, 0x61, 0x7c, 0x3d, 0xee,
0x41, 0x6a, 0x57, 0x7f, 0x1c, 0xfd, 0xc6, 0x1b, 0x5e, 0x5e, 0x57, 0x45, 0x3c, 0x65, 0x7a, 0x2a,
0x3c, 0xf4, 0xb5, 0x1b, 0x29, 0x9c, 0x0d, 0xeb, 0x5d, 0x98, 0x33, 0x68, 0x1b, 0xe1, 0xab, 0x82,
0xc1, 0x18, 0x64, 0x15, 0x6b, 0x21, 0x35, 0x68, 0x21, 0xa4, 0x6d, 0x5f, 0x47, 0x03, 0x6b, 0xfb,
0xe2, 0x4f, 0xd8, 0x54, 0x8c, 0x92, 0x04, 0xf6, 0x8a, 0xd5, 0x95, 0xc4, 0x70, 0x94, 0x24, 0x54,
0xaf, 0xe0, 0xa8, 0x76, 0x76, 0x13, 0xbd, 0x0f, 0x9c, 0x1d, 0xa5, 0x95, 0x74, 0xb8, 0x1d, 0xb6,
0xa2, 0x31, 0xe3, 0x74, 0xd8, 0x17, 0xd7, 0x8e, 0xff, 0x62, 0x2d, 0xfa, 0x2e, 0xe2, 0xf9, 0x8c,
0xcd, 0xf9, 0x92, 0x0d, 0x9e, 0x76, 0x5b, 0x53, 0xa4, 0xf1, 0xff, 0xc9, 0x2d, 0x34, 0x90, 0x61,
0x32, 0x66, 0x19, 0x9b, 0x0a, 0x72, 0x98, 0x28, 0x71, 0xe7, 0x30, 0x31, 0x98, 0x33, 0xc3, 0x1a,
0xe1, 0x01, 0x13, 0xbb, 0x8b, 0xb2, 0x64, 0xb9, 0x20, 0xfb, 0xd2, 0x22, 0x9d, 0x7d, 0xe9, 0xa1,
0x48, 0x7d, 0x0e, 0x98, 0x18, 0x65, 0x19, 0x59, 0x1f, 0x25, 0xee, 0xac, 0x8f, 0xc1, 0xb4, 0x87,
0x69, 0xf4, 0x9b, 0x4e, 0x8b, 0x89, 0xc3, 0xfc, 0x92, 0x0f, 0xe8, 0xb6, 0x90, 0x72, 0xe3, 0x63,
0xa3, 0x93, 0x43, 0xaa, 0xf1, 0xf2, 0x6d, 0xc1, 0x4b, 0xba, 0x5b, 0x94, 0xb8, 0xb3, 0x1a, 0x06,
0xd3, 0x1e, 0xfe, 0x28, 0x7a, 0x57, 0x47, 0xc9, 0x66, 0x3d, 0x7b, 0x80, 0x86, 0x50, 0xb8, 0xa0,
0x3d, 0xec, 0xa0, 0x5a, 0xe6, 0x8f, 0xd3, 0x59, 0x59, 0x47, 0x1f, 0xdc, 0xbc, 0x96, 0x76, 0x98,
0xb7, 0x94, 0x36, 0xcf, 0xa3, 0xdf, 0xf6, 0xcd, 0xef, 0xc6, 0xf9, 0x94, 0x65, 0x83, 0x27, 0x21,
0x75, 0xc5, 0x18, 0x57, 0x9b, 0xbd, 0x58, 0x1b, 0xec, 0x34, 0xa1, 0x83, 0xe9, 0xc7, 0xa8, 0x36,
0x08, 0xa5, 0x0f, 0xc2, 0x50, 0xcb, 0xf6, 0x1e, 0xcb, 0x18, 0x69, 0x5b, 0x09, 0x3b, 0x6c, 0x1b,
0x48, 0xdb, 0x2e, 0xa3, 0xdf, 0x31, 0xdd, 0x5c, 0xe7, 0x05, 0x52, 0x5e, 0x2f, 0x3a, 0x9b, 0x44,
0x3f, 0xba, 0x90, 0xf1, 0xb5, 0xd5, 0x0f, 0x6e, 0xd5, 0x47, 0x47, 0x14, 0xbc, 0x3e, 0x20, 0x9e,
0x3c, 0x08, 0x43, 0xda, 0xf6, 0xdf, 0xaf, 0x45, 0xdf, 0xd7, 0xb2, 0x97, 0x79, 0x7c, 0x91, 0x31,
0xb9, 0xc4, 0x9f, 0x30, 0x71, 0xc3, 0xcb, 0xeb, 0xf1, 0x2a, 0x9f, 0x12, 0xe9, 0x0c, 0x0e, 0x77,
0xa4, 0x33, 0xa4, 0x92, 0x2e, 0xcc, 0x9f, 0x46, 0x1f, 0x34, 0x83, 0xe2, 0x2a, 0xce, 0x67, 0xec,
0xc7, 0x15, 0xcf, 0x47, 0x45, 0x3a, 0x4a, 0x92, 0x72, 0x30, 0xc4, 0xbb, 0x1e, 0x72, 0xa6, 0x04,
0x3b, 0xbd, 0x79, 0x27, 0x7d, 0xd6, 0xad, 0x2c, 0x78, 0x01, 0xd3, 0xe7, 0xa6, 0xf9, 0x04, 0x2f,
0xa8, 0xf4, 0xd9, 0x47, 0x5a, 0x56, 0x8f, 0xeb, 0x35, 0x08, 0xb7, 0x7a, 0xec, 0x2e, 0x3a, 0xf7,
0x43, 0x88, 0x5d, 0x03, 0x9a, 0x86, 0xe2, 0xf9, 0x65, 0x3a, 0x3b, 0x2f, 0x92, 0x7a, 0x0e, 0x3d,
0xc6, 0xeb, 0xec, 0x20, 0xc4, 0x1a, 0x40, 0xa0, 0xda, 0xdb, 0x3f, 0xda, 0x2c, 0x53, 0xc7, 0xa5,
0xfd, 0x92, 0xcf, 0x8f, 0xd8, 0x2c, 0x9e, 0xae, 0x74, 0x30, 0xfd, 0x34, 0x14, 0xc5, 0x20, 0x6d,
0x0a, 0xf1, 0xd9, 0x2d, 0xb5, 0x74, 0x79, 0xfe, 0x63, 0x2d, 0x7a, 0xe0, 0x8d, 0x13, 0x3d, 0x98,
0x54, 0xe9, 0x47, 0x79, 0x72, 0xc6, 0x2a, 0x11, 0x97, 0x62, 0xf0, 0xc3, 0xc0, 0x18, 0x20, 0x74,
0x4c, 0xd9, 0x7e, 0xf4, 0x4b, 0xe9, 0xda, 0x5e, 0x1f, 0xd7, 0xab, 0x84, 0x8e, 0x3f, 0x7e, 0xaf,
0x4b, 0x09, 0x8c, 0x3e, 0xf7, 0x43, 0x88, 0xed, 0x75, 0x29, 0x38, 0xcc, 0x97, 0xa9, 0x60, 0x07,
0x2c, 0x67, 0x65, 0xbb, 0xd7, 0x95, 0xaa, 0x8f, 0x10, 0xbd, 0x4e, 0xa0, 0x36, 0xd2, 0x79, 0xde,
0x4c, 0xa6, 0xb1, 0x19, 0x30, 0xd2, 0xca, 0x35, 0xb6, 0xfa, 0xc1, 0x76, 0xab, 0xec, 0xf8, 0x3c,
0x63, 0x4b, 0x7e, 0x0d, 0xb7, 0xca, 0xae, 0x09, 0x05, 0x10, 0x5b, 0x65, 0x14, 0xb4, 0xe9, 0x80,
0xe3, 0xe7, 0x75, 0xca, 0x6e, 0x40, 0x3a, 0xe0, 0x2a, 0xd7, 0x62, 0x22, 0x1d, 0x40, 0x30, 0xed,
0xe1, 0x24, 0xfa, 0x35, 0x29, 0xfc, 0x31, 0x4f, 0xf3, 0xc1, 0x5d, 0x44, 0xa9, 0x16, 0x18, 0xab,
0xf7, 0x68, 0x00, 0x94, 0xb8, 0xfe, 0xab, 0x5e, 0x9b, 0x1f, 0x12, 0x4a, 0x60, 0x59, 0x5e, 0xef,
0xc2, 0x6c, 0x1e, 0x26, 0x85, 0x75, 0xfc, 0x1a, 0x5f, 0xc5, 0x65, 0x9a, 0xcf, 0x06, 0x98, 0xae,
0x23, 0x27, 0xf2, 0x30, 0x8c, 0x03, 0x43, 0x58, 0x2b, 0x8e, 0x8a, 0xa2, 0xac, 0xc3, 0x22, 0x36,
0x84, 0x7d, 0x24, 0x38, 0x84, 0x5b, 0x28, 0xee, 0x6d, 0x8f, 0x4d, 0xb3, 0x34, 0x0f, 0x7a, 0xd3,
0x48, 0x1f, 0x6f, 0x16, 0x05, 0x83, 0xf7, 0x88, 0xc5, 0x4b, 0xd6, 0xd4, 0x0c, 0x6b, 0x19, 0x17,
0x08, 0x0e, 0x5e, 0x00, 0xda, 0x4d, 0xaf, 0x14, 0x1f, 0xc7, 0xd7, 0xac, 0x6e, 0x60, 0x56, 0x2f,
0xaa, 0x03, 0x4c, 0xdf, 0x23, 0x88, 0x4d, 0x2f, 0x4e, 0x6a, 0x57, 0x8b, 0xe8, 0x7d, 0x29, 0x3f,
0x8d, 0x4b, 0x91, 0x4e, 0xd3, 0x22, 0xce, 0x9b, 0xcd, 0x14, 0x36, 0xaf, 0x5b, 0x94, 0x71, 0xb9,
0xdd, 0x93, 0xd6, 0x6e, 0xff, 0x7d, 0x2d, 0xfa, 0x08, 0xfa, 0x3d, 0x65, 0xe5, 0x3c, 0x95, 0x7b,
0xf2, 0x4a, 0x05, 0xe1, 0xc1, 0x17, 0x61, 0xa3, 0x2d, 0x05, 0x53, 0x9a, 0x1f, 0xdc, 0x5e, 0xd1,
0x66, 0x62, 0x63, 0xbd, 0x4f, 0x79, 0x55, 0x26, 0xad, 0x33, 0xab, 0x71, 0xb3, 0xf9, 0x90, 0x42,
0x22, 0x13, 0x6b, 0x41, 0x60, 0x86, 0x9f, 0xe7, 0x55, 0x63, 0x1d, 0x9b, 0xe1, 0x56, 0x1c, 0x9c,
0xe1, 0x1e, 0x66, 0x67, 0xf8, 0xe9, 0xe2, 0x22, 0x4b, 0xab, 0xab, 0x34, 0x9f, 0xe9, 0xb4, 0xdb,
0xd7, 0xb5, 0x62, 0x98, 0x79, 0x6f, 0x74, 0x72, 0x98, 0x13, 0x3d, 0x58, 0x48, 0x27, 0x60, 0x98,
0x6c, 0x74, 0x72, 0x76, 0x37, 0x64, 0xa5, 0xf5, 0x36, 0x1c, 0xec, 0x86, 0x1c, 0xd5, 0x5a, 0x4a,
0xec, 0x86, 0xda, 0x94, 0xdd, 0x0d, 0xb9, 0x75, 0xa8, 0x78, 0xb6, 0x64, 0xe7, 0x65, 0x0a, 0x76,
0x43, 0x5e, 0xf9, 0x1a, 0x86, 0xd8, 0x0d, 0x51, 0xac, 0x0d, 0x54, 0x96, 0x38, 0x60, 0x62, 0x2c,
0x62, 0xb1, 0xa8, 0x40, 0xa0, 0x72, 0x6c, 0x18, 0x84, 0x08, 0x54, 0x04, 0xaa, 0xbd, 0xfd, 0x61,
0x14, 0xa9, 0x13, 0x0c, 0x79, 0xca, 0xe4, 0xaf, 0x3d, 0xfa, 0x68, 0xc3, 0x3b, 0x62, 0xfa, 0x28,
0x40, 0xd8, 0x84, 0x47, 0xfd, 0x5d, 0x1e, 0x9e, 0x0d, 0x50, 0x0d, 0x29, 0x22, 0x12, 0x1e, 0x80,
0xc0, 0x82, 0x8e, 0xaf, 0xf8, 0x0d, 0x5e, 0xd0, 0x5a, 0x12, 0x2e, 0xa8, 0x26, 0xec, 0x71, 0xb6,
0x2e, 0x28, 0x76, 0x9c, 0xdd, 0x14, 0x23, 0x74, 0x9c, 0x0d, 0x19, 0x3b, 0x66, 0x5c, 0xc3, 0x2f,
0x38, 0xbf, 0x9e, 0xc7, 0xe5, 0x35, 0x18, 0x33, 0x9e, 0x72, 0xc3, 0x10, 0x63, 0x86, 0x62, 0xed,
0x98, 0x71, 0x1d, 0xd6, 0xe9, 0xf2, 0x79, 0x99, 0x81, 0x31, 0xe3, 0xd9, 0xd0, 0x08, 0x31, 0x66,
0x08, 0xd4, 0x46, 0x27, 0xd7, 0xdb, 0x98, 0xc1, 0x03, 0x14, 0x4f, 0x7d, 0xcc, 0xa8, 0x03, 0x14,
0x04, 0x83, 0x43, 0xe8, 0xa0, 0x8c, 0x8b, 0x2b, 0x7c, 0x08, 0x49, 0x51, 0x78, 0x08, 0x35, 0x08,
0xec, 0xef, 0x31, 0x8b, 0xcb, 0xe9, 0x15, 0xde, 0xdf, 0x4a, 0x16, 0xee, 0x6f, 0xc3, 0xc0, 0xfe,
0x56, 0x82, 0x37, 0xa9, 0xb8, 0x3a, 0x66, 0x22, 0xc6, 0xfb, 0xdb, 0x67, 0xc2, 0xfd, 0xdd, 0x62,
0x6d, 0x3e, 0xee, 0x3a, 0x1c, 0x2f, 0x2e, 0xaa, 0x69, 0x99, 0x5e, 0xb0, 0x41, 0xc0, 0x8a, 0x81,
0x88, 0x7c, 0x9c, 0x84, 0xb5, 0xcf, 0x9f, 0xaf, 0x45, 0x77, 0x9b, 0x6e, 0xe7, 0x55, 0xa5, 0xd7,
0x3e, 0xdf, 0xfd, 0x67, 0x78, 0xff, 0x12, 0x38, 0xf1, 0x80, 0xa1, 0x87, 0x9a, 0x93, 0x1b, 0xe0,
0x45, 0x3a, 0xcf, 0x2b, 0x53, 0xa8, 0x2f, 0xfa, 0x58, 0x77, 0x14, 0x88, 0xdc, 0xa0, 0x97, 0xa2,
0x4d, 0xcb, 0x74, 0xff, 0x34, 0xb2, 0xc3, 0xa4, 0x02, 0x69, 0x59, 0xd3, 0xde, 0x0e, 0x41, 0xa4,
0x65, 0x38, 0x09, 0x87, 0xc2, 0x41, 0xc9, 0x17, 0x45, 0xd5, 0x31, 0x14, 0x00, 0x14, 0x1e, 0x0a,
0x6d, 0x58, 0xfb, 0x7c, 0x1b, 0xfd, 0xae, 0x3b, 0xfc, 0xdc, 0xc6, 0xde, 0xa6, 0xc7, 0x14, 0xd6,
0xc4, 0xc3, 0xbe, 0xb8, 0xcd, 0x28, 0x1a, 0xcf, 0x62, 0x8f, 0x89, 0x38, 0xcd, 0xaa, 0xc1, 0x3a,
0x6e, 0xa3, 0x91, 0x13, 0x19, 0x05, 0xc6, 0xc1, 0xf8, 0xb6, 0xb7, 0x28, 0xb2, 0x74, 0xda, 0x7e,
0xbc, 0xa3, 0x75, 0x8d, 0x38, 0x1c, 0xdf, 0x5c, 0x0c, 0xc6, 0xeb, 0x3a, 0xf5, 0x93, 0xff, 0x99,
0xac, 0x0a, 0x86, 0xc7, 0x6b, 0x0f, 0x09, 0xc7, 0x6b, 0x88, 0xc2, 0xfa, 0x8c, 0x99, 0x38, 0x8a,
0x57, 0x7c, 0x41, 0xc4, 0x6b, 0x23, 0x0e, 0xd7, 0xc7, 0xc5, 0xec, 0xde, 0xc0, 0x78, 0x38, 0xcc,
0x05, 0x2b, 0xf3, 0x38, 0xdb, 0xcf, 0xe2, 0x59, 0x35, 0x20, 0x62, 0x8c, 0x4f, 0x11, 0x7b, 0x03,
0x9a, 0x46, 0x9a, 0xf1, 0xb0, 0xda, 0x8f, 0x97, 0xbc, 0x4c, 0x05, 0xdd, 0x8c, 0x16, 0xe9, 0x6c,
0x46, 0x0f, 0x45, 0xbd, 0x8d, 0xca, 0xe9, 0x55, 0xba, 0x64, 0x49, 0xc0, 0x5b, 0x83, 0xf4, 0xf0,
0xe6, 0xa0, 0x48, 0xa7, 0x8d, 0xf9, 0xa2, 0x9c, 0x32, 0xb2, 0xd3, 0x94, 0xb8, 0xb3, 0xd3, 0x0c,
0xa6, 0x3d, 0xfc, 0xf5, 0x5a, 0xf4, 0x7b, 0x4a, 0xea, 0x3e, 0x73, 0xd9, 0x8b, 0xab, 0xab, 0x0b,
0x1e, 0x97, 0xc9, 0xe0, 0x13, 0xcc, 0x0e, 0x8a, 0x1a, 0xd7, 0xcf, 0x6e, 0xa3, 0x02, 0x9b, 0xb5,
0xce, 0xbb, 0xed, 0x8c, 0x43, 0x9b, 0xd5, 0x43, 0xc2, 0xcd, 0x0a, 0x51, 0x18, 0x40, 0xa4, 0x5c,
0x1d, 0xc9, 0xad, 0x93, 0xfa, 0xfe, 0xb9, 0xdc, 0x46, 0x27, 0x07, 0xe3, 0x63, 0x2d, 0xf4, 0x47,
0xcb, 0x36, 0x65, 0x03, 0x1f, 0x31, 0xc3, 0xbe, 0x38, 0xe9, 0xd9, 0xcc, 0x8a, 0xb0, 0xe7, 0xd6,
0xcc, 0x18, 0xf6, 0xc5, 0x09, 0xcf, 0x4e, 0x58, 0x0b, 0x79, 0x46, 0x42, 0xdb, 0xb0, 0x2f, 0x0e,
0xb3, 0x2f, 0xcd, 0x34, 0xeb, 0xc2, 0x93, 0x80, 0x1d, 0xb8, 0x36, 0x6c, 0xf6, 0x62, 0xb5, 0xc3,
0xbf, 0x5d, 0x8b, 0xbe, 0x67, 0x3d, 0x1e, 0xf3, 0x24, 0xbd, 0x5c, 0x29, 0xe8, 0x75, 0x9c, 0x2d,
0x58, 0x35, 0x78, 0x46, 0x59, 0x6b, 0xb3, 0xa6, 0x04, 0xcf, 0x6f, 0xa5, 0x03, 0xe7, 0xce, 0xa8,
0x28, 0xb2, 0xd5, 0x84, 0xcd, 0x8b, 0x8c, 0x9c, 0x3b, 0x1e, 0x12, 0x9e, 0x3b, 0x10, 0x85, 0x59,
0xf9, 0x84, 0xd7, 0x39, 0x3f, 0x9a, 0x95, 0x4b, 0x51, 0x38, 0x2b, 0x6f, 0x10, 0x98, 0x2b, 0x4d,
0xf8, 0x2e, 0xcf, 0x32, 0x36, 0x15, 0xed, 0x7b, 0x1b, 0x46, 0xd3, 0x12, 0xe1, 0x5c, 0x09, 0x90,
0xf6, 0x54, 0xae, 0xd9, 0x43, 0xc6, 0x25, 0x7b, 0xb1, 0x3a, 0x4a, 0xf3, 0xeb, 0x01, 0x9e, 0x16,
0x58, 0x80, 0x38, 0x95, 0x43, 0x41, 0xb8, 0x57, 0x3d, 0xcf, 0x13, 0x8e, 0xef, 0x55, 0x6b, 0x49,
0x78, 0xaf, 0xaa, 0x09, 0x68, 0xf2, 0x8c, 0x51, 0x26, 0x6b, 0x49, 0xd8, 0xa4, 0x26, 0xb0, 0x50,
0xa8, 0x9f, 0xdd, 0x90, 0xa1, 0x10, 0x3c, 0xad, 0xd9, 0xe8, 0xe4, 0xe0, 0x08, 0x6d, 0x36, 0xad,
0xfb, 0x4c, 0x4c, 0xaf, 0xf0, 0x11, 0xea, 0x21, 0xe1, 0x11, 0x0a, 0x51, 0x58, 0xa5, 0x09, 0x37,
0x9b, 0xee, 0x75, 0x7c, 0x7c, 0xb4, 0x36, 0xdc, 0x1b, 0x9d, 0x1c, 0xdc, 0x46, 0x1e, 0xce, 0x65,
0x9b, 0xa1, 0x83, 0x5c, 0xc9, 0xc2, 0xdb, 0x48, 0xc3, 0xc0, 0xd2, 0x2b, 0x81, 0x3c, 0xcb, 0x5a,
0xa7, 0x15, 0xbd, 0xd3, 0xac, 0x8d, 0x4e, 0x4e, 0x3b, 0xf9, 0x57, 0xb3, 0x8d, 0x53, 0xd2, 0x13,
0x5e, 0xcf, 0x91, 0xd7, 0x71, 0x96, 0x26, 0xb1, 0x60, 0x13, 0x7e, 0xcd, 0x72, 0x7c, 0xc7, 0xa4,
0x4b, 0xab, 0xf8, 0xa1, 0xa7, 0x10, 0xde, 0x31, 0x85, 0x15, 0xe1, 0x38, 0x51, 0xf4, 0x79, 0xc5,
0x76, 0xe3, 0x8a, 0x88, 0x64, 0x1e, 0x12, 0x1e, 0x27, 0x10, 0x85, 0xf9, 0xaa, 0x92, 0xbf, 0x7c,
0x5b, 0xb0, 0x32, 0x65, 0xf9, 0x94, 0xe1, 0xf9, 0x2a, 0xa4, 0xc2, 0xf9, 0x2a, 0x42, 0xc3, 0xbd,
0xda, 0x5e, 0x2c, 0xd8, 0x8b, 0xd5, 0x24, 0x9d, 0xb3, 0x4a, 0xc4, 0xf3, 0x02, 0xdf, 0xab, 0x01,
0x28, 0xbc, 0x57, 0x6b, 0xc3, 0xad, 0xa3, 0x21, 0x13, 0x10, 0xdb, 0xd7, 0xbd, 0x20, 0x11, 0xb8,
0xee, 0x45, 0xa0, 0xb0, 0x61, 0x2d, 0x80, 0x3e, 0x24, 0x68, 0x59, 0x09, 0x3e, 0x24, 0xa0, 0xe9,
0xd6, 0x81, 0x9b, 0x61, 0xc6, 0xf5, 0xd4, 0xec, 0x28, 0xfa, 0xd8, 0x9d, 0xa2, 0x9b, 0xbd, 0x58,
0xfc, 0x84, 0xef, 0x8c, 0x65, 0xb1, 0x5c, 0xb6, 0x02, 0xc7, 0x68, 0x0d, 0xd3, 0xe7, 0x84, 0xcf,
0x61, 0xb5, 0xc3, 0xbf, 0x5c, 0x8b, 0x3e, 0xc4, 0x3c, 0xbe, 0x2a, 0xa4, 0xdf, 0xa7, 0xdd, 0xb6,
0x14, 0x49, 0xdc, 0x67, 0x0b, 0x6b, 0xd8, 0x2b, 0x19, 0x8d, 0xc8, 0x5e, 0x77, 0xd3, 0x05, 0xf0,
0x93, 0x36, 0x53, 0x7e, 0xc8, 0x11, 0x57, 0x32, 0x42, 0xbc, 0xdd, 0x0f, 0xf9, 0xe5, 0xaa, 0xc0,
0x7e, 0xc8, 0xd8, 0xd0, 0x62, 0x62, 0x3f, 0x84, 0x60, 0x76, 0x76, 0xba, 0xd5, 0x7b, 0x93, 0x8a,
0x2b, 0x99, 0x6f, 0x81, 0xd9, 0xe9, 0x95, 0xd5, 0x40, 0xc4, 0xec, 0x24, 0x61, 0x98, 0x91, 0x34,
0x60, 0x3d, 0x37, 0xb1, 0x58, 0x6e, 0x0c, 0xb9, 0x33, 0xf3, 0x51, 0x37, 0x08, 0xc7, 0x6b, 0x23,
0xd6, 0x5b, 0x9f, 0x27, 0x21, 0x0b, 0x60, 0xfb, 0xb3, 0xd9, 0x8b, 0xd5, 0x0e, 0xff, 0x3c, 0xfa,
0x6e, 0xab, 0x62, 0xfb, 0x2c, 0x16, 0x8b, 0x92, 0x25, 0x83, 0x9d, 0x8e, 0x72, 0x37, 0xa0, 0x71,
0xfd, 0xb4, 0xbf, 0x42, 0x2b, 0x47, 0x6f, 0x38, 0x35, 0xac, 0x4c, 0x19, 0x9e, 0x85, 0x4c, 0xfa,
0x6c, 0x30, 0x47, 0xa7, 0x75, 0x5a, 0xdb, 0x6c, 0x77, 0x74, 0x8d, 0x96, 0x71, 0x9a, 0xc9, 0x87,
0xb5, 0x9f, 0x84, 0x8c, 0x7a, 0x68, 0x70, 0x9b, 0x4d, 0xaa, 0xb4, 0x22, 0xb3, 0x9c, 0xe3, 0xce,
0xf6, 0x6c, 0x8b, 0x8e, 0x04, 0xc8, 0xee, 0x6c, 0xbb, 0x27, 0xad, 0xdd, 0x8a, 0x66, 0xc9, 0xab,
0xff, 0xec, 0x0e, 0x72, 0xcc, 0xab, 0x56, 0x45, 0x46, 0xfa, 0x76, 0x4f, 0x5a, 0x7b, 0xfd, 0xb3,
0xe8, 0x83, 0xb6, 0x57, 0xbd, 0x10, 0xed, 0x74, 0x9a, 0x02, 0x6b, 0xd1, 0xd3, 0xfe, 0x0a, 0x76,
0x4b, 0xf3, 0x65, 0x5a, 0x09, 0x5e, 0xae, 0xc6, 0x57, 0xfc, 0xa6, 0x79, 0x8d, 0xc4, 0x9f, 0xad,
0x1a, 0x18, 0x3a, 0x04, 0xb1, 0xa5, 0xc1, 0xc9, 0x96, 0x2b, 0xfb, 0xba, 0x49, 0x45, 0xb8, 0x72,
0x88, 0x0e, 0x57, 0x3e, 0x69, 0x63, 0x55, 0x53, 0x2b, 0xfb, 0x6e, 0xcc, 0x06, 0x5e, 0xd4, 0xf6,
0xfb, 0x31, 0x8f, 0xba, 0x41, 0x9b, 0xb1, 0x68, 0xf1, 0x5e, 0x7a, 0x79, 0x69, 0xea, 0x84, 0x97,
0xd4, 0x45, 0x88, 0x8c, 0x85, 0x40, 0x6d, 0xd2, 0xbd, 0x9f, 0x66, 0x4c, 0x9e, 0xe8, 0xbf, 0xba,
0xbc, 0xcc, 0x78, 0x9c, 0x80, 0xa4, 0xbb, 0x16, 0x0f, 0x5d, 0x39, 0x91, 0x74, 0x63, 0x9c, 0xbd,
0x2b, 0x50, 0x4b, 0xcf, 0xd8, 0x94, 0xe7, 0xd3, 0x34, 0x83, 0xb7, 0x50, 0xa5, 0xa6, 0x11, 0x12,
0x77, 0x05, 0x5a, 0x90, 0x5d, 0x18, 0x6b, 0x51, 0x3d, 0xed, 0x9b, 0xf2, 0x3f, 0x6c, 0x2b, 0x3a,
0x62, 0x62, 0x61, 0x44, 0x30, 0xbb, 0xf7, 0xac, 0x85, 0xe7, 0x85, 0x34, 0x7e, 0xaf, 0xad, 0xa5,
0x24, 0xc4, 0xde, 0xd3, 0x27, 0xec, 0x1e, 0xaa, 0xfe, 0xfb, 0x1e, 0xbf, 0xc9, 0xa5, 0xd1, 0xfb,
0x6d, 0x95, 0x46, 0x46, 0xec, 0xa1, 0x20, 0xa3, 0x0d, 0xff, 0x24, 0xfa, 0x55, 0x69, 0xb8, 0xe4,
0xc5, 0xe0, 0x0e, 0xa2, 0x50, 0x3a, 0x77, 0x36, 0xef, 0x92, 0x72, 0x7b, 0xb5, 0xc0, 0x8c, 0x8d,
0xf3, 0x2a, 0x9e, 0xc1, 0x8b, 0xd6, 0xb6, 0xc7, 0xa5, 0x94, 0xb8, 0x5a, 0xd0, 0xa6, 0xfc, 0x51,
0x71, 0xc2, 0x13, 0x6d, 0x1d, 0xa9, 0xa1, 0x11, 0x86, 0x46, 0x85, 0x0b, 0xd9, 0x64, 0xe6, 0x24,
0x5e, 0xa6, 0x33, 0xb3, 0xe0, 0xa8, 0xb8, 0x55, 0x81, 0x64, 0xc6, 0x32, 0x43, 0x07, 0x22, 0x92,
0x19, 0x12, 0xd6, 0x3e, 0xff, 0x65, 0x2d, 0xba, 0x67, 0x99, 0x83, 0xe6, 0xb4, 0xee, 0x30, 0xbf,
0xe4, 0x75, 0xea, 0x73, 0x94, 0xe6, 0xd7, 0xd5, 0xe0, 0x73, 0xca, 0x24, 0xce, 0x9b, 0xa2, 0x7c,
0x71, 0x6b, 0x3d, 0x9b, 0xb5, 0x36, 0x47, 0x59, 0xf6, 0x79, 0xb6, 0xd2, 0x00, 0x59, 0xab, 0x39,
0xf1, 0x82, 0x1c, 0x91, 0xb5, 0x86, 0x78, 0xdb, 0xc5, 0xc6, 0x79, 0xc6, 0x73, 0xd8, 0xc5, 0xd6,
0x42, 0x2d, 0x24, 0xba, 0xb8, 0x05, 0xd9, 0x78, 0xdc, 0x88, 0xd4, 0xa9, 0xcb, 0x28, 0xcb, 0x40,
0x3c, 0x36, 0xaa, 0x06, 0x20, 0xe2, 0x31, 0x0a, 0x6a, 0x3f, 0x67, 0xd1, 0xb7, 0xeb, 0x26, 0x3d,
0x2d, 0xd9, 0x32, 0x65, 0xf0, 0xea, 0x85, 0x23, 0x21, 0xe6, 0xbf, 0x4f, 0xd8, 0x99, 0x75, 0x9e,
0x57, 0x45, 0x16, 0x57, 0x57, 0xfa, 0x61, 0xbc, 0x5f, 0xe7, 0x46, 0x08, 0x1f, 0xc7, 0x3f, 0xec,
0xa0, 0x6c, 0x50, 0x6f, 0x64, 0x26, 0xc4, 0xac, 0xe3, 0xaa, 0xad, 0x30, 0xb3, 0xd1, 0xc9, 0xd9,
0x13, 0xef, 0x83, 0x38, 0xcb, 0x58, 0xb9, 0x6a, 0x64, 0xc7, 0x71, 0x9e, 0x5e, 0xb2, 0x4a, 0x80,
0x13, 0x6f, 0x4d, 0x0d, 0x21, 0x46, 0x9c, 0x78, 0x07, 0x70, 0x9b, 0xcd, 0x03, 0xcf, 0x87, 0x79,
0xc2, 0xde, 0x82, 0x6c, 0x1e, 0xda, 0x91, 0x0c, 0x91, 0xcd, 0x53, 0xac, 0x3d, 0xf9, 0x7d, 0x91,
0xf1, 0xe9, 0xb5, 0x5e, 0x02, 0xfc, 0x0e, 0x96, 0x12, 0xb8, 0x06, 0xdc, 0x0f, 0x21, 0x76, 0x11,
0x90, 0x82, 0x33, 0x56, 0x64, 0xf1, 0x14, 0xde, 0xbf, 0x51, 0x3a, 0x5a, 0x46, 0x2c, 0x02, 0x90,
0x01, 0xc5, 0xd5, 0xf7, 0x7a, 0xb0, 0xe2, 0x82, 0x6b, 0x3d, 0xf7, 0x43, 0x88, 0x5d, 0x06, 0xa5,
0x60, 0x5c, 0x64, 0xa9, 0x00, 0xd3, 0x40, 0x69, 0x48, 0x09, 0x31, 0x0d, 0x7c, 0x02, 0x98, 0x3c,
0x66, 0xe5, 0x8c, 0xa1, 0x26, 0xa5, 0x24, 0x68, 0xb2, 0x21, 0xec, 0x65, 0x63, 0x55, 0x77, 0x5e,
0xac, 0xc0, 0x65, 0x63, 0x5d, 0x2d, 0x5e, 0xac, 0x88, 0xcb, 0xc6, 0x1e, 0x00, 0x8a, 0x78, 0x1a,
0x57, 0x02, 0x2f, 0xa2, 0x94, 0x04, 0x8b, 0xd8, 0x10, 0x76, 0x8d, 0x56, 0x45, 0x5c, 0x08, 0xb0,
0x46, 0xeb, 0x02, 0x38, 0x4f, 0xa0, 0xef, 0x92, 0x72, 0x1b, 0x49, 0x54, 0xaf, 0x30, 0xb1, 0x9f,
0xb2, 0x2c, 0xa9, 0x40, 0x24, 0xd1, 0xed, 0xde, 0x48, 0x89, 0x48, 0xd2, 0xa6, 0xc0, 0x50, 0xd2,
0xe7, 0xe3, 0x58, 0xed, 0xc0, 0xd1, 0xf8, 0xfd, 0x10, 0x62, 0xe3, 0x53, 0x53, 0xe8, 0xdd, 0xb8,
0x2c, 0xd3, 0x7a, 0xf1, 0x5f, 0xc7, 0x0b, 0xd4, 0xc8, 0x89, 0xf8, 0x84, 0x71, 0x60, 0x7a, 0x35,
0x81, 0x1b, 0x2b, 0x18, 0x0c, 0xdd, 0x1f, 0x07, 0x19, 0x9b, 0x71, 0x4a, 0x89, 0xf3, 0x08, 0x15,
0x6b, 0x4d, 0xe4, 0x09, 0xea, 0x7a, 0x17, 0xe6, 0xbc, 0x89, 0x64, 0x5c, 0x1c, 0xf3, 0x25, 0x9b,
0xf0, 0x97, 0x6f, 0xd3, 0x4a, 0xa4, 0xf9, 0x4c, 0xaf, 0xdc, 0xcf, 0x09, 0x4b, 0x18, 0x4c, 0xbc,
0x89, 0xd4, 0xa9, 0x64, 0x13, 0x08, 0x50, 0x96, 0x13, 0x76, 0x83, 0x26, 0x10, 0xd0, 0xa2, 0xe1,
0x88, 0x04, 0x22, 0xc4, 0xdb, 0x73, 0x14, 0xe3, 0x5c, 0xbf, 0x7e, 0x3e, 0xe1, 0x4d, 0x2e, 0x47,
0x59, 0x83, 0x20, 0xb1, 0x95, 0x0d, 0x2a, 0xd8, 0xfd, 0xa5, 0xf1, 0x6f, 0xa7, 0xd8, 0x23, 0xc2,
0x4e, 0x7b, 0x9a, 0x3d, 0xee, 0x41, 0x22, 0xae, 0xec, 0x3d, 0x00, 0xca, 0x55, 0xfb, 0x1a, 0xc0,
0xe3, 0x1e, 0xa4, 0x73, 0x26, 0xe3, 0x56, 0xeb, 0x45, 0x3c, 0xbd, 0x9e, 0x95, 0x7c, 0x91, 0x27,
0xbb, 0x3c, 0xe3, 0x25, 0x38, 0x93, 0xf1, 0x4a, 0x0d, 0x50, 0xe2, 0x4c, 0xa6, 0x43, 0xc5, 0x66,
0x70, 0x6e, 0x29, 0x46, 0x59, 0x3a, 0x83, 0x3b, 0x6a, 0xcf, 0x90, 0x04, 0x88, 0x0c, 0x0e, 0x05,
0x91, 0x41, 0xa4, 0x76, 0xdc, 0x22, 0x9d, 0xc6, 0x99, 0xf2, 0xb7, 0x43, 0x9b, 0xf1, 0xc0, 0xce,
0x41, 0x84, 0x28, 0x20, 0xf5, 0x9c, 0x2c, 0xca, 0xfc, 0x30, 0x17, 0x9c, 0xac, 0x67, 0x03, 0x74,
0xd6, 0xd3, 0x01, 0x41, 0x58, 0x9d, 0xb0, 0xb7, 0x75, 0x69, 0xea, 0x7f, 0xb0, 0xb0, 0x5a, 0xff,
0x7d, 0xa8, 0xe5, 0xa1, 0xb0, 0x0a, 0x38, 0x50, 0x19, 0xed, 0x44, 0x0d, 0x98, 0x80, 0xb6, 0x3f,
0x4c, 0x1e, 0x75, 0x83, 0xb8, 0x9f, 0xb1, 0x58, 0x65, 0x2c, 0xe4, 0x47, 0x02, 0x7d, 0xfc, 0x34,
0xa0, 0x3d, 0x6e, 0xf1, 0xea, 0x73, 0xc5, 0xa6, 0xd7, 0xad, 0x6b, 0x4d, 0x7e, 0x41, 0x15, 0x42,
0x1c, 0xb7, 0x10, 0x28, 0xde, 0x45, 0x87, 0x53, 0x9e, 0x87, 0xba, 0xa8, 0x96, 0xf7, 0xe9, 0x22,
0xcd, 0xd9, 0xcd, 0xaf, 0x91, 0xea, 0x91, 0xa9, 0xba, 0x69, 0x93, 0xb0, 0xe0, 0x42, 0xc4, 0xe6,
0x97, 0x84, 0x6d, 0x4e, 0x0e, 0x7d, 0x1e, 0xb7, 0xef, 0x7c, 0xb7, 0xac, 0x1c, 0xd3, 0x77, 0xbe,
0x29, 0x96, 0xae, 0xa4, 0x1a, 0x23, 0x1d, 0x56, 0xfc, 0x71, 0xb2, 0xd5, 0x0f, 0xb6, 0x5b, 0x1e,
0xcf, 0xe7, 0x6e, 0xc6, 0xe2, 0x52, 0x79, 0xdd, 0x0e, 0x18, 0xb2, 0x18, 0xb1, 0xe5, 0x09, 0xe0,
0x20, 0x84, 0x79, 0x9e, 0x77, 0x79, 0x2e, 0x58, 0x2e, 0xb0, 0x10, 0xe6, 0x1b, 0xd3, 0x60, 0x28,
0x84, 0x51, 0x0a, 0x60, 0xdc, 0xca, 0xf3, 0x20, 0x26, 0x4e, 0xe2, 0x39, 0x9a, 0xb1, 0xa9, 0xb3,
0x1e, 0x25, 0x0f, 0x8d, 0x5b, 0xc0, 0x39, 0x0f, 0xf9, 0x5c, 0x2f, 0x93, 0xb8, 0x9c, 0x99, 0xd3,
0x8d, 0x64, 0xf0, 0x94, 0xb6, 0xe3, 0x93, 0xc4, 0x43, 0xbe, 0xb0, 0x06, 0x08, 0x3b, 0x87, 0xf3,
0x78, 0x66, 0x6a, 0x8a, 0xd4, 0x40, 0xca, 0x5b, 0x55, 0x7d, 0xd4, 0x0d, 0x02, 0x3f, 0xaf, 0xd3,
0x84, 0xf1, 0x80, 0x1f, 0x29, 0xef, 0xe3, 0x07, 0x82, 0x20, 0x7b, 0xab, 0xeb, 0xad, 0x76, 0x74,
0xa3, 0x3c, 0xd1, 0xfb, 0xd8, 0x21, 0xd1, 0x3c, 0x80, 0x0b, 0x65, 0x6f, 0x04, 0x0f, 0xe6, 0x68,
0x73, 0x40, 0x1b, 0x9a, 0xa3, 0xe6, 0xfc, 0xb5, 0xcf, 0x1c, 0xc5, 0x60, 0xed, 0xf3, 0x67, 0x7a,
0x8e, 0xee, 0xc5, 0x22, 0xae, 0xf3, 0xf6, 0xd7, 0x29, 0xbb, 0xd1, 0x1b, 0x61, 0xa4, 0xbe, 0x0d,
0x35, 0x94, 0xaf, 0xac, 0x82, 0x5d, 0xf1, 0x4e, 0x6f, 0x3e, 0xe0, 0x5b, 0xef, 0x10, 0x3a, 0x7d,
0x83, 0xad, 0xc2, 0x4e, 0x6f, 0x3e, 0xe0, 0x5b, 0xbf, 0x0b, 0xdf, 0xe9, 0x1b, 0xbc, 0x10, 0xbf,
0xd3, 0x9b, 0xd7, 0xbe, 0xff, 0xaa, 0x99, 0xb8, 0xae, 0xf3, 0x3a, 0x0f, 0x9b, 0x8a, 0x74, 0xc9,
0xb0, 0x74, 0xd2, 0xb7, 0x67, 0xd0, 0x50, 0x3a, 0x49, 0xab, 0x38, 0x5f, 0xa3, 0xc2, 0x4a, 0x71,
0xca, 0xab, 0x54, 0x3e, 0xa4, 0x7f, 0xde, 0xc3, 0x68, 0x03, 0x87, 0x36, 0x4d, 0x21, 0x25, 0xfb,
0xb8, 0xd1, 0x43, 0xed, 0x2d, 0xe6, 0xad, 0x80, 0xbd, 0xf6, 0x65, 0xe6, 0xed, 0x9e, 0xb4, 0x7d,
0xf0, 0xe7, 0x31, 0xee, 0x13, 0xc7, 0x50, 0xaf, 0xa2, 0x0f, 0x1d, 0x9f, 0xf6, 0x57, 0xd0, 0xee,
0xff, 0xa6, 0xd9, 0x57, 0x40, 0xff, 0x7a, 0x12, 0x3c, 0xeb, 0x63, 0x11, 0x4c, 0x84, 0xe7, 0xb7,
0xd2, 0xd1, 0x05, 0xf9, 0x87, 0x66, 0x03, 0xdd, 0xa0, 0xf2, 0x5d, 0x0e, 0xf9, 0x0e, 0xa8, 0x9e,
0x13, 0xa1, 0x6e, 0xb5, 0x30, 0x9c, 0x19, 0x9f, 0xdd, 0x52, 0xcb, 0xf9, 0x36, 0x99, 0x07, 0xeb,
0x77, 0x0e, 0x9d, 0xf2, 0x84, 0x2c, 0x3b, 0x34, 0x2c, 0xd0, 0xe7, 0xb7, 0x55, 0xa3, 0xe6, 0x8a,
0x03, 0xcb, 0xaf, 0x73, 0x3c, 0xef, 0x69, 0xd8, 0xfb, 0x5e, 0xc7, 0xa7, 0xb7, 0x53, 0xd2, 0x65,
0xf9, 0xcf, 0xb5, 0xe8, 0xa1, 0xc7, 0xda, 0xe7, 0x09, 0xe0, 0xd4, 0xe3, 0x47, 0x01, 0xfb, 0x94,
0x92, 0x29, 0xdc, 0xef, 0xff, 0x72, 0xca, 0xf6, 0x43, 0x5e, 0x9e, 0xca, 0x7e, 0x9a, 0x09, 0x56,
0xb6, 0x3f, 0xe4, 0xe5, 0xdb, 0x55, 0xd4, 0x90, 0xfe, 0x90, 0x57, 0x00, 0x77, 0x3e, 0xe4, 0x85,
0x78, 0x46, 0x3f, 0xe4, 0x85, 0x5a, 0x0b, 0x7e, 0xc8, 0x2b, 0xac, 0x41, 0x85, 0xf7, 0xa6, 0x08,
0xea, 0xdc, 0xba, 0x97, 0x45, 0xff, 0x18, 0xfb, 0xd9, 0x6d, 0x54, 0x88, 0x05, 0x4e, 0x71, 0xf2,
0x9e, 0x5b, 0x8f, 0x36, 0xf5, 0xee, 0xba, 0xed, 0xf4, 0xe6, 0xb5, 0xef, 0x9f, 0xea, 0xdd, 0x8d,
0x09, 0xe7, 0xbc, 0x94, 0x1f, 0x71, 0xdb, 0x0c, 0x85, 0xe7, 0xda, 0x82, 0xdb, 0xf3, 0x5b, 0xfd,
0x60, 0xa2, 0xba, 0x35, 0xa1, 0x3b, 0x7d, 0xd8, 0x65, 0x08, 0x74, 0xf9, 0x4e, 0x6f, 0x9e, 0x58,
0x46, 0x94, 0x6f, 0xd5, 0xdb, 0x3d, 0x8c, 0xf9, 0x7d, 0xfd, 0xb4, 0xbf, 0x82, 0x76, 0xbf, 0xd4,
0x69, 0xa3, 0xeb, 0x5e, 0xf6, 0xf3, 0x76, 0x97, 0xa9, 0xb1, 0xd7, 0xcd, 0xc3, 0xbe, 0x78, 0x28,
0x81, 0x70, 0x97, 0xd0, 0xae, 0x04, 0x02, 0x5d, 0x46, 0x3f, 0xbd, 0x9d, 0x92, 0x2e, 0xcb, 0x3f,
0xaf, 0x45, 0x77, 0xc9, 0xb2, 0xe8, 0x71, 0xf0, 0x79, 0x5f, 0xcb, 0x60, 0x3c, 0x7c, 0x71, 0x6b,
0x3d, 0x5d, 0xa8, 0x7f, 0x5b, 0x8b, 0xee, 0x05, 0x0a, 0xa5, 0x06, 0xc8, 0x2d, 0xac, 0xfb, 0x03,
0xe5, 0x07, 0xb7, 0x57, 0xa4, 0x96, 0x7b, 0x17, 0x1f, 0xb7, 0x3f, 0xca, 0x14, 0xb0, 0x3d, 0xa6,
0x3f, 0xca, 0xd4, 0xad, 0x05, 0x0f, 0x79, 0xe2, 0x8b, 0x66, 0xd3, 0x85, 0x1e, 0xf2, 0xc8, 0x1b,
0x6a, 0xc1, 0x8f, 0x4b, 0x60, 0x1c, 0xe6, 0xe4, 0xe5, 0xdb, 0x22, 0xce, 0x13, 0xda, 0x89, 0x92,
0x77, 0x3b, 0x31, 0x1c, 0x3c, 0x1c, 0xab, 0xa5, 0x67, 0xbc, 0xd9, 0x48, 0x3d, 0xa6, 0xf4, 0x0d,
0x12, 0x3c, 0x1c, 0x6b, 0xa1, 0x84, 0x37, 0x9d, 0x35, 0x86, 0xbc, 0x81, 0x64, 0xf1, 0x49, 0x1f,
0x14, 0xa4, 0xe8, 0xc6, 0x9b, 0x39, 0x73, 0xdf, 0x0a, 0x59, 0x69, 0x9d, 0xbb, 0x6f, 0xf7, 0xa4,
0x09, 0xb7, 0x63, 0x26, 0xbe, 0x64, 0x71, 0xc2, 0xca, 0xa0, 0x5b, 0x43, 0xf5, 0x72, 0xeb, 0xd2,
0x98, 0xdb, 0x5d, 0x9e, 0x2d, 0xe6, 0xb9, 0xee, 0x4c, 0xd2, 0xad, 0x4b, 0x75, 0xbb, 0x05, 0x34,
0x3c, 0x16, 0xb4, 0x6e, 0x65, 0x7a, 0xf9, 0x24, 0x6c, 0xc6, 0xcb, 0x2a, 0x37, 0x7b, 0xb1, 0x74,
0x3d, 0xf5, 0x30, 0xea, 0xa8, 0x27, 0x18, 0x49, 0xdb, 0x3d, 0x69, 0x78, 0x3e, 0xe7, 0xb8, 0x35,
0xe3, 0x69, 0xa7, 0xc3, 0x56, 0x6b, 0x48, 0x3d, 0xed, 0xaf, 0x00, 0x4f, 0x43, 0xf5, 0xa8, 0x3a,
0x4a, 0x2b, 0xb1, 0x9f, 0x66, 0xd9, 0x60, 0x33, 0x30, 0x4c, 0x1a, 0x28, 0x78, 0x1a, 0x8a, 0xc0,
0xc4, 0x48, 0x6e, 0x4e, 0x0f, 0xf3, 0x41, 0x97, 0x1d, 0x49, 0xf5, 0x1a, 0xc9, 0x2e, 0x0d, 0x4e,
0xb4, 0x9c, 0xa6, 0x36, 0xb5, 0x1d, 0x86, 0x1b, 0xae, 0x55, 0xe1, 0x9d, 0xde, 0x3c, 0x78, 0xdc,
0x2e, 0x29, 0xb9, 0xb2, 0x3c, 0xa0, 0x4c, 0x78, 0x2b, 0xc9, 0xc3, 0x0e, 0x0a, 0x9c, 0x0a, 0xaa,
0x69, 0xf4, 0x26, 0x4d, 0x66, 0x4c, 0xa0, 0x4f, 0x8a, 0x5c, 0x20, 0xf8, 0xa4, 0x08, 0x80, 0xa0,
0xeb, 0xd4, 0xdf, 0xcd, 0x71, 0xe8, 0x61, 0x82, 0x75, 0x9d, 0x56, 0x76, 0xa8, 0x50, 0xd7, 0xa1,
0x34, 0x88, 0x06, 0xc6, 0xad, 0x7e, 0x1d, 0xff, 0x49, 0xc8, 0x0c, 0x78, 0x27, 0x7f, 0xb3, 0x17,
0x0b, 0x56, 0x14, 0xeb, 0x30, 0x9d, 0xa7, 0x02, 0x5b, 0x51, 0x1c, 0x1b, 0x35, 0x12, 0x5a, 0x51,
0xda, 0x28, 0x55, 0xbd, 0x3a, 0x47, 0x38, 0x4c, 0xc2, 0xd5, 0x53, 0x4c, 0xbf, 0xea, 0x19, 0xb6,
0xf5, 0x60, 0x33, 0x37, 0x43, 0x46, 0x5c, 0xe9, 0xcd, 0x32, 0x32, 0xb6, 0xe5, 0x6b, 0x9a, 0x10,
0x0c, 0x45, 0x1d, 0x4a, 0x01, 0x1e, 0xd8, 0xd7, 0x5c, 0xf3, 0xec, 0xb5, 0x28, 0x58, 0x5c, 0xc6,
0xf9, 0x14, 0xdd, 0x9c, 0x4a, 0x83, 0x2d, 0x32, 0xb4, 0x39, 0x25, 0x35, 0xc0, 0x63, 0x73, 0xff,
0x05, 0x4b, 0x64, 0x2a, 0x98, 0x37, 0x19, 0xfd, 0xf7, 0x2b, 0x1f, 0xf7, 0x20, 0xe1, 0x63, 0xf3,
0x06, 0x30, 0x07, 0xdf, 0xca, 0xe9, 0x27, 0x01, 0x53, 0x3e, 0x1a, 0xda, 0x08, 0xd3, 0x2a, 0x60,
0x50, 0x9b, 0x04, 0x97, 0x89, 0x9f, 0xb0, 0x15, 0x36, 0xa8, 0x6d, 0x7e, 0x2a, 0x91, 0xd0, 0xa0,
0x6e, 0xa3, 0x20, 0xcf, 0x74, 0xf7, 0x41, 0xeb, 0x01, 0x7d, 0x77, 0xeb, 0xb3, 0xd1, 0xc9, 0x81,
0x99, 0xb3, 0x97, 0x2e, 0xbd, 0xe7, 0x04, 0x48, 0x41, 0xf7, 0xd2, 0x25, 0xfe, 0x98, 0x60, 0xb3,
0x17, 0x0b, 0x1f, 0xc9, 0xc7, 0x82, 0xbd, 0x6d, 0x9e, 0x95, 0x23, 0xc5, 0x95, 0xf2, 0xd6, 0xc3,
0xf2, 0x47, 0xdd, 0xa0, 0xbd, 0x00, 0x7b, 0x5a, 0xf2, 0x29, 0xab, 0x2a, 0xfd, 0xa5, 0x4a, 0xff,
0x86, 0x91, 0x96, 0x0d, 0xc1, 0x77, 0x2a, 0x1f, 0x84, 0x21, 0xe7, 0xf3, 0x72, 0x4a, 0x64, 0xbf,
0x7a, 0xb3, 0x8e, 0x6a, 0xb6, 0x3f, 0x78, 0xb3, 0xd1, 0xc9, 0xd9, 0xe9, 0xa5, 0xa5, 0xee, 0x67,
0x6e, 0x1e, 0xa1, 0xea, 0xd8, 0x17, 0x6e, 0x1e, 0xf7, 0x20, 0xb5, 0xab, 0x2f, 0xa3, 0x77, 0x8e,
0xf8, 0x6c, 0xcc, 0xf2, 0x64, 0xf0, 0x7d, 0xff, 0x0a, 0x2d, 0x9f, 0x0d, 0xeb, 0x3f, 0x1b, 0xa3,
0x77, 0x28, 0xb1, 0xbd, 0x04, 0xb8, 0xc7, 0x2e, 0x16, 0xb3, 0xb1, 0x88, 0x05, 0xb8, 0x04, 0x28,
0xff, 0x3e, 0xac, 0x05, 0xc4, 0x25, 0x40, 0x0f, 0x00, 0xf6, 0x26, 0x25, 0x63, 0xa8, 0xbd, 0x5a,
0x10, 0xb4, 0xa7, 0x01, 0x9b, 0x45, 0x18, 0x7b, 0x75, 0xa2, 0x0e, 0x2f, 0xed, 0x59, 0x1d, 0x29,
0x25, 0xb2, 0x88, 0x36, 0x65, 0x07, 0xb7, 0xaa, 0xbe, 0xfc, 0xea, 0xc8, 0x62, 0x3e, 0x8f, 0xcb,
0x15, 0x18, 0xdc, 0xba, 0x96, 0x0e, 0x40, 0x0c, 0x6e, 0x14, 0xb4, 0xb3, 0xb6, 0x69, 0xe6, 0xe9,
0xf5, 0x01, 0x2f, 0xf9, 0x42, 0xa4, 0x39, 0x83, 0x5f, 0x9e, 0x30, 0x0d, 0xea, 0x32, 0xc4, 0xac,
0xa5, 0x58, 0x9b, 0xe5, 0x4a, 0x42, 0xdd, 0x27, 0x94, 0x1f, 0xcf, 0xae, 0x04, 0x2f, 0xe1, 0xf3,
0x44, 0x65, 0x05, 0x42, 0x44, 0x96, 0x4b, 0xc2, 0xa0, 0xef, 0x4f, 0xd3, 0x7c, 0x86, 0xf6, 0xfd,
0xa9, 0xfb, 0xf5, 0xd7, 0x7b, 0x34, 0x60, 0x27, 0x94, 0x6a, 0x34, 0x35, 0x01, 0xf4, 0xbb, 0x9c,
0x68, 0xa3, 0xbb, 0x04, 0x31, 0xa1, 0x70, 0x12, 0xb8, 0x7a, 0x55, 0xb0, 0x9c, 0x25, 0xcd, 0xad,
0x39, 0xcc, 0x95, 0x47, 0x04, 0x5d, 0x41, 0xd2, 0xc6, 0x22, 0x29, 0x3f, 0x5b, 0xe4, 0xa7, 0x25,
0xbf, 0x4c, 0x33, 0x56, 0x82, 0x58, 0xa4, 0xd4, 0x1d, 0x39, 0x11, 0x8b, 0x30, 0xce, 0x5e, 0xbf,
0x90, 0x52, 0xef, 0x0b, 0xf0, 0x93, 0x32, 0x9e, 0xc2, 0xeb, 0x17, 0xca, 0x46, 0x1b, 0x23, 0x4e,
0x06, 0x03, 0xb8, 0x93, 0xe8, 0x28, 0xd7, 0xf9, 0x4a, 0x8e, 0x0f, 0xfd, 0x2e, 0xa1, 0xfc, 0x26,
0x6a, 0x05, 0x12, 0x1d, 0x6d, 0x0e, 0x23, 0x89, 0x44, 0x27, 0xac, 0x61, 0x97, 0x12, 0xc9, 0x9d,
0xe8, 0x6b, 0x45, 0x60, 0x29, 0x51, 0x36, 0x1a, 0x21, 0xb1, 0x94, 0xb4, 0x20, 0x10, 0x90, 0x9a,
0x69, 0x30, 0x43, 0x03, 0x92, 0x91, 0x06, 0x03, 0x92, 0x4b, 0xd9, 0x40, 0x71, 0x98, 0xa7, 0x22,
0x8d, 0xb3, 0x31, 0x13, 0xa7, 0x71, 0x19, 0xcf, 0x99, 0x60, 0x25, 0x0c, 0x14, 0x1a, 0x19, 0x7a,
0x0c, 0x11, 0x28, 0x28, 0x56, 0x3b, 0xfc, 0x83, 0xe8, 0xbd, 0x7a, 0xdd, 0x67, 0xb9, 0xfe, 0xed,
0x9a, 0x97, 0xf2, 0x47, 0xaf, 0x06, 0xef, 0x1b, 0x1b, 0x63, 0x51, 0xb2, 0x78, 0xde, 0xd8, 0x7e,
0xd7, 0xfc, 0x5d, 0x82, 0x4f, 0xd7, 0xea, 0xf1, 0x7c, 0xc2, 0x45, 0x7a, 0x59, 0x6f, 0xb3, 0xf5,
0x1b, 0x44, 0x60, 0x3c, 0xbb, 0xe2, 0x61, 0xe0, 0x5b, 0x14, 0x18, 0x67, 0xe3, 0xb4, 0x2b, 0x3d,
0x63, 0x45, 0x06, 0xe3, 0xb4, 0xa7, 0x2d, 0x01, 0x22, 0x4e, 0xa3, 0xa0, 0x9d, 0x9c, 0xae, 0x78,
0xc2, 0xc2, 0x95, 0x99, 0xb0, 0x7e, 0x95, 0x99, 0x78, 0x2f, 0x65, 0x64, 0xd1, 0x7b, 0xc7, 0x6c,
0x7e, 0xc1, 0xca, 0xea, 0x2a, 0x2d, 0xa8, 0xef, 0xb6, 0x5a, 0xa2, 0xf3, 0xbb, 0xad, 0x04, 0x6a,
0x57, 0x02, 0x0b, 0x1c, 0x56, 0x27, 0xf1, 0x9c, 0xc9, 0x2f, 0x6b, 0x80, 0x95, 0xc0, 0x31, 0xe2,
0x40, 0xc4, 0x4a, 0x40, 0xc2, 0xce, 0xfb, 0x5d, 0x96, 0x39, 0x63, 0xb3, 0x7a, 0x84, 0x95, 0xa7,
0xf1, 0x6a, 0xce, 0x72, 0xa1, 0x4d, 0x82, 0x33, 0x79, 0xc7, 0x24, 0xce, 0x13, 0x67, 0xf2, 0x7d,
0xf4, 0x9c, 0xd0, 0xe4, 0x35, 0xfc, 0x29, 0x2f, 0x85, 0xfa, 0x65, 0xaa, 0xf3, 0x32, 0x03, 0xa1,
0xc9, 0x6f, 0x54, 0x8f, 0x24, 0x42, 0x53, 0x58, 0xc3, 0xf9, 0x15, 0x02, 0xaf, 0x0c, 0xaf, 0x59,
0x69, 0xc6, 0xc9, 0xcb, 0x79, 0x9c, 0x66, 0x7a, 0x34, 0xfc, 0x30, 0x60, 0x9b, 0xd0, 0x21, 0x7e,
0x85, 0xa0, 0xaf, 0xae, 0xf3, 0xbb, 0x0d, 0xe1, 0x12, 0x82, 0x47, 0x04, 0x1d, 0xf6, 0x89, 0x47,
0x04, 0xdd, 0x5a, 0x76, 0xe7, 0x6e, 0x59, 0xc9, 0xad, 0x24, 0xb1, 0xcb, 0x13, 0x78, 0x5e, 0xe8,
0xd8, 0x04, 0x20, 0xb1, 0x73, 0x0f, 0x2a, 0xd8, 0xd4, 0xc0, 0x62, 0xfb, 0x69, 0x1e, 0x67, 0xe9,
0xcf, 0x60, 0x5a, 0xef, 0xd8, 0x69, 0x08, 0x22, 0x35, 0xc0, 0x49, 0xcc, 0xd5, 0x01, 0x13, 0x93,
0xb4, 0x0e, 0xfd, 0x8f, 0x02, 0xed, 0x26, 0x89, 0x6e, 0x57, 0x0e, 0xe9, 0x7c, 0xa3, 0x15, 0x36,
0xeb, 0xa8, 0x28, 0xc6, 0xf5, 0xaa, 0x7a, 0xc6, 0xa6, 0x2c, 0x2d, 0xc4, 0xe0, 0xb3, 0x70, 0x5b,
0x01, 0x9c, 0xb8, 0x68, 0xd1, 0x43, 0xcd, 0x79, 0x7c, 0x5f, 0xc7, 0x92, 0xb1, 0xfa, 0xc9, 0xc6,
0xf3, 0x8a, 0x95, 0x3a, 0xd1, 0x38, 0x60, 0x02, 0xcc, 0x4e, 0x87, 0x1b, 0x3a, 0x60, 0x5d, 0x51,
0x62, 0x76, 0x86, 0x35, 0xec, 0x61, 0x9f, 0xc3, 0xe9, 0x6f, 0x6e, 0xcb, 0xfb, 0x86, 0x5b, 0xa4,
0x31, 0x87, 0x22, 0x0e, 0xfb, 0x68, 0xda, 0x66, 0x6b, 0x6d, 0xb7, 0xa3, 0x7c, 0x75, 0x08, 0xaf,
0x4c, 0x20, 0x96, 0x24, 0x46, 0x64, 0x6b, 0x01, 0xdc, 0x39, 0x0c, 0x2f, 0x79, 0x9c, 0x4c, 0xe3,
0x4a, 0x9c, 0xc6, 0xab, 0x8c, 0xc7, 0x89, 0x5c, 0xd7, 0xe1, 0x61, 0x78, 0xc3, 0x0c, 0x5d, 0x88,
0x3a, 0x0c, 0xa7, 0x60, 0x37, 0x3b, 0x93, 0xbf, 0x44, 0xa9, 0xef, 0x72, 0xc2, 0xec, 0x4c, 0x96,
0x17, 0xde, 0xe3, 0x7c, 0x10, 0x86, 0xec, 0x3b, 0x68, 0x4a, 0x24, 0xd3, 0x90, 0x7b, 0x98, 0x8e,
0x97, 0x80, 0x7c, 0x14, 0x20, 0xec, 0x77, 0x29, 0xd4, 0xdf, 0x9b, 0x1f, 0x1f, 0x12, 0xfa, 0x4b,
0xd6, 0x5b, 0x98, 0xae, 0x0b, 0x0d, 0xdd, 0x0f, 0xdc, 0x6d, 0xf7, 0xa4, 0x6d, 0x9a, 0xb9, 0x7b,
0x15, 0x8b, 0x51, 0x92, 0x1c, 0xb3, 0x0a, 0x79, 0xa1, 0xbc, 0x16, 0x0e, 0xad, 0x94, 0x48, 0x33,
0xdb, 0x94, 0x1d, 0xe8, 0xb5, 0xec, 0x65, 0x92, 0x0a, 0x2d, 0x6b, 0x6e, 0x48, 0x6f, 0xb5, 0x0d,
0xb4, 0x29, 0xa2, 0x56, 0x34, 0x6d, 0x63, 0x79, 0xcd, 0x4c, 0xf8, 0x6c, 0x96, 0x31, 0x0d, 0x9d,
0xb1, 0x58, 0x7d, 0xc8, 0x6f, 0xa7, 0x6d, 0x0b, 0x05, 0x89, 0x58, 0x1e, 0x54, 0xb0, 0x69, 0x64,
0x8d, 0xa9, 0x47, 0x52, 0x4d, 0xc3, 0x6e, 0xb4, 0xcd, 0x78, 0x00, 0x91, 0x46, 0xa2, 0xa0, 0x7d,
0xef, 0xad, 0x16, 0x1f, 0xb0, 0xa6, 0x25, 0xe0, 0x27, 0x88, 0xa4, 0xb2, 0x23, 0x26, 0xde, 0x7b,
0x43, 0x30, 0xbb, 0x4f, 0x00, 0x1e, 0x5e, 0xac, 0x0e, 0x13, 0xb8, 0x4f, 0x80, 0xfa, 0x92, 0x21,
0xf6, 0x09, 0x14, 0xeb, 0x77, 0x9d, 0x39, 0xf7, 0x3a, 0x8a, 0x2b, 0x5b, 0x39, 0xa4, 0xeb, 0x50,
0x30, 0xd4, 0x75, 0x94, 0x82, 0xdf, 0xa4, 0xee, 0xd1, 0x1a, 0xd2, 0xa4, 0xd8, 0xb9, 0xda, 0x7a,
0x17, 0xe6, 0x24, 0x3e, 0x5e, 0x15, 0x27, 0x5c, 0x17, 0x43, 0xbf, 0xd7, 0x58, 0x81, 0xc4, 0xc7,
0x2f, 0x76, 0x8b, 0x26, 0x12, 0x9f, 0x6e, 0x2d, 0x1b, 0x27, 0xcd, 0xfe, 0x56, 0x5e, 0xa1, 0xc2,
0x7f, 0x51, 0x40, 0x09, 0x89, 0x38, 0xd9, 0x82, 0x94, 0xed, 0x17, 0x1f, 0xfd, 0xd7, 0xd7, 0x77,
0xd6, 0x7e, 0xf1, 0xf5, 0x9d, 0xb5, 0xff, 0xfd, 0xfa, 0xce, 0xda, 0xcf, 0xbf, 0xb9, 0xf3, 0xad,
0x5f, 0x7c, 0x73, 0xe7, 0x5b, 0xff, 0xf3, 0xcd, 0x9d, 0x6f, 0x7d, 0xf5, 0x8e, 0xfe, 0xc5, 0xe4,
0x8b, 0x5f, 0x91, 0xbf, 0x7b, 0xfc, 0xfc, 0xff, 0x03, 0x00, 0x00, 0xff, 0xff, 0x5a, 0x7a, 0x62,
0x93, 0x55, 0x79, 0x00, 0x00,
}
// This is a compile-time assertion to ensure that this generated file
@ -399,11 +403,14 @@ type ClientCommandsHandler interface {
// Account
// ***
AccountRecover(context.Context, *pb.RpcAccountRecoverRequest) *pb.RpcAccountRecoverResponse
AccountMigrate(context.Context, *pb.RpcAccountMigrateRequest) *pb.RpcAccountMigrateResponse
AccountMigrateCancel(context.Context, *pb.RpcAccountMigrateCancelRequest) *pb.RpcAccountMigrateCancelResponse
AccountCreate(context.Context, *pb.RpcAccountCreateRequest) *pb.RpcAccountCreateResponse
AccountDelete(context.Context, *pb.RpcAccountDeleteRequest) *pb.RpcAccountDeleteResponse
AccountRevertDeletion(context.Context, *pb.RpcAccountRevertDeletionRequest) *pb.RpcAccountRevertDeletionResponse
AccountSelect(context.Context, *pb.RpcAccountSelectRequest) *pb.RpcAccountSelectResponse
AccountEnableLocalNetworkSync(context.Context, *pb.RpcAccountEnableLocalNetworkSyncRequest) *pb.RpcAccountEnableLocalNetworkSyncResponse
AccountChangeJsonApiAddr(context.Context, *pb.RpcAccountChangeJsonApiAddrRequest) *pb.RpcAccountChangeJsonApiAddrResponse
AccountStop(context.Context, *pb.RpcAccountStopRequest) *pb.RpcAccountStopResponse
AccountMove(context.Context, *pb.RpcAccountMoveRequest) *pb.RpcAccountMoveResponse
AccountConfigUpdate(context.Context, *pb.RpcAccountConfigUpdateRequest) *pb.RpcAccountConfigUpdateResponse
@ -1137,6 +1144,46 @@ func AccountRecover(b []byte) (resp []byte) {
return resp
}
func AccountMigrate(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcAccountMigrateResponse{Error: &pb.RpcAccountMigrateResponseError{Code: pb.RpcAccountMigrateResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcAccountMigrateRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcAccountMigrateResponse{Error: &pb.RpcAccountMigrateResponseError{Code: pb.RpcAccountMigrateResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.AccountMigrate(context.Background(), in).Marshal()
return resp
}
func AccountMigrateCancel(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcAccountMigrateCancelResponse{Error: &pb.RpcAccountMigrateCancelResponseError{Code: pb.RpcAccountMigrateCancelResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcAccountMigrateCancelRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcAccountMigrateCancelResponse{Error: &pb.RpcAccountMigrateCancelResponseError{Code: pb.RpcAccountMigrateCancelResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.AccountMigrateCancel(context.Background(), in).Marshal()
return resp
}
func AccountCreate(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
@ -1237,6 +1284,26 @@ func AccountEnableLocalNetworkSync(b []byte) (resp []byte) {
return resp
}
func AccountChangeJsonApiAddr(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcAccountChangeJsonApiAddrResponse{Error: &pb.RpcAccountChangeJsonApiAddrResponseError{Code: pb.RpcAccountChangeJsonApiAddrResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcAccountChangeJsonApiAddrRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcAccountChangeJsonApiAddrResponse{Error: &pb.RpcAccountChangeJsonApiAddrResponseError{Code: pb.RpcAccountChangeJsonApiAddrResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.AccountChangeJsonApiAddr(context.Background(), in).Marshal()
return resp
}
func AccountStop(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
@ -6265,6 +6332,10 @@ func CommandAsync(cmd string, data []byte, callback func(data []byte)) {
cd = WorkspaceExport(data)
case "AccountRecover":
cd = AccountRecover(data)
case "AccountMigrate":
cd = AccountMigrate(data)
case "AccountMigrateCancel":
cd = AccountMigrateCancel(data)
case "AccountCreate":
cd = AccountCreate(data)
case "AccountDelete":
@ -6275,6 +6346,8 @@ func CommandAsync(cmd string, data []byte, callback func(data []byte)) {
cd = AccountSelect(data)
case "AccountEnableLocalNetworkSync":
cd = AccountEnableLocalNetworkSync(data)
case "AccountChangeJsonApiAddr":
cd = AccountChangeJsonApiAddr(data)
case "AccountStop":
cd = AccountStop(data)
case "AccountMove":
@ -7089,6 +7162,34 @@ func (h *ClientCommandsHandlerProxy) AccountRecover(ctx context.Context, req *pb
call, _ := actualCall(ctx, req)
return call.(*pb.RpcAccountRecoverResponse)
}
func (h *ClientCommandsHandlerProxy) AccountMigrate(ctx context.Context, req *pb.RpcAccountMigrateRequest) *pb.RpcAccountMigrateResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.AccountMigrate(ctx, req.(*pb.RpcAccountMigrateRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "AccountMigrate", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcAccountMigrateResponse)
}
func (h *ClientCommandsHandlerProxy) AccountMigrateCancel(ctx context.Context, req *pb.RpcAccountMigrateCancelRequest) *pb.RpcAccountMigrateCancelResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.AccountMigrateCancel(ctx, req.(*pb.RpcAccountMigrateCancelRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "AccountMigrateCancel", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcAccountMigrateCancelResponse)
}
func (h *ClientCommandsHandlerProxy) AccountCreate(ctx context.Context, req *pb.RpcAccountCreateRequest) *pb.RpcAccountCreateResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.AccountCreate(ctx, req.(*pb.RpcAccountCreateRequest)), nil
@ -7159,6 +7260,20 @@ func (h *ClientCommandsHandlerProxy) AccountEnableLocalNetworkSync(ctx context.C
call, _ := actualCall(ctx, req)
return call.(*pb.RpcAccountEnableLocalNetworkSyncResponse)
}
func (h *ClientCommandsHandlerProxy) AccountChangeJsonApiAddr(ctx context.Context, req *pb.RpcAccountChangeJsonApiAddrRequest) *pb.RpcAccountChangeJsonApiAddrResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.AccountChangeJsonApiAddr(ctx, req.(*pb.RpcAccountChangeJsonApiAddrRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "AccountChangeJsonApiAddr", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcAccountChangeJsonApiAddrResponse)
}
func (h *ClientCommandsHandlerProxy) AccountStop(ctx context.Context, req *pb.RpcAccountStopRequest) *pb.RpcAccountStopResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.AccountStop(ctx, req.(*pb.RpcAccountStopRequest)), nil

View file

@ -9,16 +9,19 @@ import (
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/goccy/go-graphviz"
"github.com/gogo/protobuf/jsonpb"
"github.com/gogo/protobuf/proto"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/debug/treearchive"
"github.com/anyproto/anytype-heart/core/debug/exporter"
"github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
@ -42,22 +45,20 @@ func main() {
return
}
fmt.Println("opening file...")
st := time.Now()
archive, err := treearchive.Open(*file)
var (
st = time.Now()
ctx = context.Background()
)
res, err := exporter.ImportStorage(ctx, *file)
if err != nil {
log.Fatal("can't open debug file:", err)
log.Fatal("can't import the tree:", err)
}
objectTree, err := res.CreateReadableTree(*fromRoot, "")
if err != nil {
log.Fatal("can't create readable tree:", err)
}
defer archive.Close()
fmt.Printf("open archive done in %.1fs\n", time.Since(st).Seconds())
importer := treearchive.NewTreeImporter(archive.ListStorage(), archive.TreeStorage())
st = time.Now()
err = importer.Import(*fromRoot, "")
if err != nil {
log.Fatal("can't import the tree", err)
}
fmt.Printf("import tree done in %.1fs\n", time.Since(st).Seconds())
importer := exporter.NewTreeImporter(objectTree)
if *makeJson {
treeJson, err := importer.Json()
if err != nil {
@ -83,10 +84,11 @@ func main() {
}
fmt.Println("Change:")
fmt.Println(pbtypes.Sprint(ch.Model))
err = importer.Import(*fromRoot, ch.Id)
objectTree, err = res.CreateReadableTree(*fromRoot, ch.Id)
if err != nil {
log.Fatal("can't import the tree before", ch.Id, err)
log.Fatal("can't create readable tree:", err)
}
importer = exporter.NewTreeImporter(objectTree)
}
ot := importer.ObjectTree()
di, err := ot.Debug(state.ChangeParser{})
@ -126,12 +128,16 @@ func main() {
if *objectStore {
fmt.Println("fetch object store info..")
ls, err := archive.LocalStore()
f, err := os.Open(filepath.Join(res.FolderPath, "localstore.json"))
if err != nil {
fmt.Println("can't open objectStore info:", err)
} else {
fmt.Println(pbtypes.Sprint(ls))
log.Fatal("can't open objectStore info:", err)
}
defer f.Close()
info := &model.ObjectInfo{}
if err = jsonpb.Unmarshal(f, info); err != nil {
log.Fatal("can't unmarshal objectStore info:", err)
}
fmt.Println(pbtypes.Sprint(info))
}
if *makeTree {

View file

@ -31,6 +31,7 @@ import (
"google.golang.org/grpc"
"github.com/anyproto/anytype-heart/core"
"github.com/anyproto/anytype-heart/core/api"
"github.com/anyproto/anytype-heart/core/event"
"github.com/anyproto/anytype-heart/metrics"
"github.com/anyproto/anytype-heart/pb"
@ -225,6 +226,7 @@ func main() {
}()
startReportMemory(mw)
api.SetMiddlewareParams(mw)
shutdown := func() {
server.Stop()

View file

@ -306,7 +306,7 @@ func getTableSizes(mw *core.Middleware) (tables map[string]uint64) {
tables = make(map[string]uint64)
cfg := mw.GetApp().MustComponent(config.CName).(*config.Config)
db, err := sql.Open("sqlite3", cfg.GetSpaceStorePath())
db, err := sql.Open("sqlite3", cfg.GetSqliteStorePath())
if err != nil {
fmt.Println("Error opening database:", err)
return

View file

@ -10,6 +10,7 @@ import (
"github.com/anyproto/anytype-heart/core/application"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/space/spacecore/storage/migrator"
)
func (mw *Middleware) AccountCreate(cctx context.Context, req *pb.RpcAccountCreateRequest) *pb.RpcAccountCreateResponse {
@ -49,6 +50,35 @@ func (mw *Middleware) AccountRecover(cctx context.Context, _ *pb.RpcAccountRecov
}
}
func (mw *Middleware) AccountMigrate(cctx context.Context, req *pb.RpcAccountMigrateRequest) *pb.RpcAccountMigrateResponse {
err := mw.applicationService.AccountMigrate(cctx, req)
code := mapErrorCode(err,
errToCode(application.ErrBadInput, pb.RpcAccountMigrateResponseError_BAD_INPUT),
errToCode(application.ErrAccountNotFound, pb.RpcAccountMigrateResponseError_ACCOUNT_NOT_FOUND),
errToCode(context.Canceled, pb.RpcAccountMigrateResponseError_CANCELED),
errTypeToCode(&migrator.NotEnoughFreeSpaceError{}, pb.RpcAccountMigrateResponseError_NOT_ENOUGH_FREE_SPACE),
)
return &pb.RpcAccountMigrateResponse{
Error: &pb.RpcAccountMigrateResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
func (mw *Middleware) AccountMigrateCancel(cctx context.Context, req *pb.RpcAccountMigrateCancelRequest) *pb.RpcAccountMigrateCancelResponse {
err := mw.applicationService.AccountMigrateCancel(cctx, req)
code := mapErrorCode(err,
errToCode(application.ErrBadInput, pb.RpcAccountMigrateCancelResponseError_BAD_INPUT),
)
return &pb.RpcAccountMigrateCancelResponse{
Error: &pb.RpcAccountMigrateCancelResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
func (mw *Middleware) AccountSelect(cctx context.Context, req *pb.RpcAccountSelectRequest) *pb.RpcAccountSelectResponse {
account, err := mw.applicationService.AccountSelect(cctx, req)
code := mapErrorCode(err,
@ -64,6 +94,7 @@ func (mw *Middleware) AccountSelect(cctx context.Context, req *pb.RpcAccountSele
errToCode(application.ErrAnotherProcessIsRunning, pb.RpcAccountSelectResponseError_ANOTHER_ANYTYPE_PROCESS_IS_RUNNING),
errToCode(application.ErrIncompatibleVersion, pb.RpcAccountSelectResponseError_FAILED_TO_FETCH_REMOTE_NODE_HAS_INCOMPATIBLE_PROTO_VERSION),
errToCode(application.ErrFailedToStartApplication, pb.RpcAccountSelectResponseError_FAILED_TO_RUN_NODE),
errToCode(application.ErrAccountStoreIsNotMigrated, pb.RpcAccountSelectResponseError_ACCOUNT_STORE_NOT_MIGRATED),
)
return &pb.RpcAccountSelectResponse{
Config: nil,
@ -198,10 +229,23 @@ func (mw *Middleware) AccountEnableLocalNetworkSync(_ context.Context, req *pb.R
}
}
func (mw *Middleware) AccountChangeJsonApiAddr(ctx context.Context, req *pb.RpcAccountChangeJsonApiAddrRequest) *pb.RpcAccountChangeJsonApiAddrResponse {
err := mw.applicationService.AccountChangeJsonApiAddr(ctx, req.ListenAddr)
code := mapErrorCode(err,
errToCode(application.ErrApplicationIsNotRunning, pb.RpcAccountChangeJsonApiAddrResponseError_ACCOUNT_IS_NOT_RUNNING),
)
return &pb.RpcAccountChangeJsonApiAddrResponse{
Error: &pb.RpcAccountChangeJsonApiAddrResponseError{
Code: code,
Description: getErrorDescription(err),
},
}
}
func (mw *Middleware) AccountLocalLinkNewChallenge(ctx context.Context, request *pb.RpcAccountLocalLinkNewChallengeRequest) *pb.RpcAccountLocalLinkNewChallengeResponse {
info := getClientInfo(ctx)
challengeId, err := mw.applicationService.LinkLocalStartNewChallenge(&info)
challengeId, err := mw.applicationService.LinkLocalStartNewChallenge(request.Scope, &info)
code := mapErrorCode(err,
errToCode(session.ErrTooManyChallengeRequests, pb.RpcAccountLocalLinkNewChallengeResponseError_TOO_MANY_REQUESTS),
errToCode(application.ErrApplicationIsNotRunning, pb.RpcAccountLocalLinkNewChallengeResponseError_ACCOUNT_IS_NOT_RUNNING),

View file

@ -10,7 +10,6 @@ import (
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/commonspace/acl/aclclient"
"github.com/anyproto/any-sync/commonspace/object/acl/list"
"github.com/anyproto/any-sync/commonspace/object/acl/liststorage"
"github.com/anyproto/any-sync/coordinator/coordinatorclient"
"github.com/anyproto/any-sync/coordinator/coordinatorproto"
"github.com/anyproto/any-sync/nodeconf"
@ -392,7 +391,7 @@ func (a *aclService) ViewInvite(ctx context.Context, inviteCid cid.Cid, inviteFi
if len(recs) == 0 {
return domain.InviteView{}, fmt.Errorf("no acl records found for space: %s, %w", res.SpaceId, ErrAclRequestFailed)
}
store, err := liststorage.NewInMemoryAclListStorage(recs[0].Id, recs)
store, err := list.NewInMemoryStorage(recs[0].Id, recs)
if err != nil {
return domain.InviteView{}, convertedOrAclRequestError(err)
}

View file

@ -35,6 +35,7 @@ import (
"github.com/anyproto/anytype-heart/core/acl"
"github.com/anyproto/anytype-heart/core/anytype/account"
"github.com/anyproto/anytype-heart/core/anytype/config"
"github.com/anyproto/anytype-heart/core/api"
"github.com/anyproto/anytype-heart/core/block"
"github.com/anyproto/anytype-heart/core/block/backlinks"
"github.com/anyproto/anytype-heart/core/block/bookmark"
@ -45,7 +46,6 @@ import (
"github.com/anyproto/anytype-heart/core/block/detailservice"
"github.com/anyproto/anytype-heart/core/block/editor"
"github.com/anyproto/anytype-heart/core/block/editor/converter"
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
"github.com/anyproto/anytype-heart/core/block/export"
importer "github.com/anyproto/anytype-heart/core/block/import"
"github.com/anyproto/anytype-heart/core/block/object/idderiver/idderiverimpl"
@ -82,7 +82,6 @@ import (
paymentscache "github.com/anyproto/anytype-heart/core/payments/cache"
"github.com/anyproto/anytype-heart/core/peerstatus"
"github.com/anyproto/anytype-heart/core/publish"
"github.com/anyproto/anytype-heart/core/recordsbatcher"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/core/spaceview"
"github.com/anyproto/anytype-heart/core/subscription"
@ -109,9 +108,12 @@ import (
"github.com/anyproto/anytype-heart/space/spacecore/clientserver"
"github.com/anyproto/anytype-heart/space/spacecore/credentialprovider"
"github.com/anyproto/anytype-heart/space/spacecore/localdiscovery"
"github.com/anyproto/anytype-heart/space/spacecore/oldstorage"
"github.com/anyproto/anytype-heart/space/spacecore/peermanager"
"github.com/anyproto/anytype-heart/space/spacecore/peerstore"
"github.com/anyproto/anytype-heart/space/spacecore/storage"
"github.com/anyproto/anytype-heart/space/spacecore/storage/migrator"
"github.com/anyproto/anytype-heart/space/spacecore/storage/migratorfinisher"
"github.com/anyproto/anytype-heart/space/spacecore/typeprovider"
"github.com/anyproto/anytype-heart/space/spacefactory"
"github.com/anyproto/anytype-heart/space/virtualspaceservice"
@ -207,6 +209,18 @@ func appVersion(a *app.App, clientWithVersion string) string {
return clientWithVersion + "/middle:" + middleVersion + "/any-sync:" + anySyncVersion
}
func BootstrapMigration(a *app.App, components ...app.Component) {
for _, c := range components {
a.Register(c)
}
a.Register(migratorfinisher.New()).
Register(clientds.New()).
Register(oldstorage.New()).
Register(storage.New()).
Register(process.New()).
Register(migrator.New())
}
func Bootstrap(a *app.App, components ...app.Component) {
for _, c := range components {
a.Register(c)
@ -278,7 +292,6 @@ func Bootstrap(a *app.App, components ...app.Component) {
Register(acl.New()).
Register(builtintemplate.New()).
Register(converter.NewLayoutConverter()).
Register(recordsbatcher.New()).
Register(configfetcher.New()).
Register(process.New()).
Register(core.NewTempDirService()).
@ -320,8 +333,8 @@ func Bootstrap(a *app.App, components ...app.Component) {
Register(payments.New()).
Register(paymentscache.New()).
Register(peerstatus.New()).
Register(lastused.New()).
Register(spaceview.New())
Register(spaceview.New()).
Register(api.New())
}
func MiddlewareVersion() string {

View file

@ -43,6 +43,7 @@ const (
const (
SpaceStoreBadgerPath = "spacestore"
SpaceStoreSqlitePath = "spaceStore.db"
SpaceStoreNewPath = "spaceStoreNew"
)
var (
@ -69,11 +70,13 @@ type Config struct {
DisableThreadsSyncEvents bool
DontStartLocalNetworkSyncAutomatically bool
PeferYamuxTransport bool
DisableNetworkIdCheck bool
SpaceStorageMode storage.SpaceStorageMode
NetworkMode pb.RpcAccountNetworkMode
NetworkCustomConfigFilePath string `json:",omitempty"` // not saved to config
SqliteTempPath string `json:",omitempty"` // not saved to config
AnyStoreConfig *anystore.Config `json:",omitempty"` // not saved to config
JsonApiListenAddr string `json:",omitempty"` // empty means disabled
RepoPath string
AnalyticsId string
@ -292,12 +295,27 @@ func (c *Config) FSConfig() (FSConfig, error) {
return FSConfig{IPFSStorageAddr: res.CustomFileStorePath}, nil
}
func (c *Config) GetRepoPath() string {
return c.RepoPath
}
func (c *Config) GetConfigPath() string {
return filepath.Join(c.RepoPath, ConfigFileName)
}
func (c *Config) GetSpaceStorePath() string {
return filepath.Join(c.RepoPath, "spaceStore.db")
func (c *Config) GetSqliteStorePath() string {
return filepath.Join(c.RepoPath, SpaceStoreSqlitePath)
}
func (c *Config) GetOldSpaceStorePath() string {
if c.GetSpaceStorageMode() == storage.SpaceStorageModeBadger {
return filepath.Join(c.RepoPath, SpaceStoreBadgerPath)
}
return c.GetSqliteStorePath()
}
func (c *Config) GetNewSpaceStorePath() string {
return filepath.Join(c.RepoPath, SpaceStoreNewPath)
}
func (c *Config) GetTempDirPath() string {
@ -390,7 +408,7 @@ func (c *Config) GetNodeConfWithError() (conf nodeconf.Configuration, err error)
if err := yaml.Unmarshal(confBytes, &conf); err != nil {
return nodeconf.Configuration{}, errors.Join(ErrNetworkFileFailedToRead, err)
}
if c.NetworkId != "" && c.NetworkId != conf.NetworkId {
if !c.DisableNetworkIdCheck && c.NetworkId != "" && c.NetworkId != conf.NetworkId {
log.Warnf("Network id mismatch: %s != %s", c.NetworkId, conf.NetworkId)
return nodeconf.Configuration{}, errors.Join(ErrNetworkIdMismatch, fmt.Errorf("network id mismatch: %s != %s", c.NetworkId, conf.NetworkId))
}

1638
core/api/docs/docs.go Normal file

File diff suppressed because it is too large Load diff

1614
core/api/docs/swagger.json Normal file

File diff suppressed because it is too large Load diff

1108
core/api/docs/swagger.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,75 @@
package auth
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/anyproto/anytype-heart/core/api/util"
)
// DisplayCodeHandler starts a new challenge and returns the challenge ID
//
// @Summary Start new challenge
// @Tags auth
// @Accept json
// @Produce json
// @Param app_name query string true "App name requesting the challenge"
// @Success 200 {object} DisplayCodeResponse "Challenge ID"
// @Failure 400 {object} util.ValidationError "Invalid input"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /auth/display_code [post]
func DisplayCodeHandler(s *AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
appName := c.Query("app_name")
challengeId, err := s.NewChallenge(c.Request.Context(), appName)
code := util.MapErrorCode(err,
util.ErrToCode(ErrMissingAppName, http.StatusBadRequest),
util.ErrToCode(ErrFailedGenerateChallenge, http.StatusInternalServerError))
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, DisplayCodeResponse{ChallengeId: challengeId})
}
}
// TokenHandler retrieves an authentication token using a code and challenge ID
//
// @Summary Retrieve token
// @Tags auth
// @Accept json
// @Produce json
// @Param challenge_id query string true "Challenge ID"
// @Param code query string true "4-digit code retrieved from Anytype Desktop app"
// @Success 200 {object} TokenResponse "Authentication token"
// @Failure 400 {object} util.ValidationError "Invalid input"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /auth/token [post]
func TokenHandler(s *AuthService) gin.HandlerFunc {
return func(c *gin.Context) {
challengeId := c.Query("challenge_id")
code := c.Query("code")
sessionToken, appKey, err := s.SolveChallenge(c.Request.Context(), challengeId, code)
errCode := util.MapErrorCode(err,
util.ErrToCode(ErrInvalidInput, http.StatusBadRequest),
util.ErrToCode(ErrFailedAuthenticate, http.StatusInternalServerError),
)
if errCode != http.StatusOK {
apiErr := util.CodeToAPIError(errCode, err.Error())
c.JSON(errCode, apiErr)
return
}
c.JSON(http.StatusOK, TokenResponse{
SessionToken: sessionToken,
AppKey: appKey,
})
}
}

View file

@ -0,0 +1,10 @@
package auth
type DisplayCodeResponse struct {
ChallengeId string `json:"challenge_id" example:"67647f5ecda913e9a2e11b26"`
}
type TokenResponse struct {
SessionToken string `json:"session_token" example:"eyJhbGciOeJIRzI1NiIsInR5cCI6IkpXVCJ1.eyJzZWVkIjaiY0dmVndlUnAifQ.Y1EZecYnwmvMkrXKOa2XJnAbaRt34urBabe06tmDQII"`
AppKey string `json:"app_key" example:"zhSG/zQRmgADyilWPtgdnfo1qD60oK02/SVgi1GaFt6="`
}

View file

@ -0,0 +1,67 @@
package auth
import (
"context"
"errors"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
var (
ErrMissingAppName = errors.New("missing app name")
ErrFailedGenerateChallenge = errors.New("failed to generate a new challenge")
ErrInvalidInput = errors.New("invalid input")
ErrFailedAuthenticate = errors.New("failed to authenticate user")
)
type Service interface {
NewChallenge(ctx context.Context, appName string) (string, error)
SolveChallenge(ctx context.Context, challengeId string, code string) (sessionToken, appKey string, err error)
}
type AuthService struct {
mw service.ClientCommandsServer
}
func NewService(mw service.ClientCommandsServer) *AuthService {
return &AuthService{mw: mw}
}
// NewChallenge calls AccountLocalLinkNewChallenge and returns the challenge ID, or an error if it fails.
func (s *AuthService) NewChallenge(ctx context.Context, appName string) (string, error) {
if appName == "" {
return "", ErrMissingAppName
}
resp := s.mw.AccountLocalLinkNewChallenge(ctx, &pb.RpcAccountLocalLinkNewChallengeRequest{
AppName: appName,
Scope: model.AccountAuth_JsonAPI,
})
if resp.Error.Code != pb.RpcAccountLocalLinkNewChallengeResponseError_NULL {
return "", ErrFailedGenerateChallenge
}
return resp.ChallengeId, nil
}
// SolveChallenge calls AccountLocalLinkSolveChallenge and returns the session token + app key, or an error if it fails.
func (s *AuthService) SolveChallenge(ctx context.Context, challengeId string, code string) (sessionToken string, appKey string, err error) {
if challengeId == "" || code == "" {
return "", "", ErrInvalidInput
}
// Call AccountLocalLinkSolveChallenge to retrieve session token and app key
resp := s.mw.AccountLocalLinkSolveChallenge(ctx, &pb.RpcAccountLocalLinkSolveChallengeRequest{
ChallengeId: challengeId,
Answer: code,
})
if resp.Error.Code != pb.RpcAccountLocalLinkSolveChallengeResponseError_NULL {
return "", "", ErrFailedAuthenticate
}
return resp.SessionToken, resp.AppKey, nil
}

View file

@ -0,0 +1,161 @@
package auth
import (
"context"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service/mock_service"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
const (
mockedAppName = "api-test"
mockedChallengeId = "mocked-challenge-id"
mockedCode = "mocked-mockedCode"
mockedSessionToken = "mocked-session-token"
mockedAppKey = "mocked-app-key"
)
type fixture struct {
*AuthService
mwMock *mock_service.MockClientCommandsServer
}
func newFixture(t *testing.T) *fixture {
mw := mock_service.NewMockClientCommandsServer(t)
authService := NewService(mw)
return &fixture{
AuthService: authService,
mwMock: mw,
}
}
func TestAuthService_GenerateNewChallenge(t *testing.T) {
t.Run("successful challenge creation", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("AccountLocalLinkNewChallenge", mock.Anything, &pb.RpcAccountLocalLinkNewChallengeRequest{
AppName: mockedAppName,
Scope: model.AccountAuth_JsonAPI,
}).
Return(&pb.RpcAccountLocalLinkNewChallengeResponse{
ChallengeId: mockedChallengeId,
Error: &pb.RpcAccountLocalLinkNewChallengeResponseError{Code: pb.RpcAccountLocalLinkNewChallengeResponseError_NULL},
}).Once()
// when
challengeId, err := fx.NewChallenge(ctx, mockedAppName)
// then
require.NoError(t, err)
require.Equal(t, mockedChallengeId, challengeId)
})
t.Run("bad request - missing app name", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
// when
challengeId, err := fx.NewChallenge(ctx, "")
// then
require.Error(t, err)
require.Equal(t, ErrMissingAppName, err)
require.Empty(t, challengeId)
})
t.Run("failed challenge creation", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("AccountLocalLinkNewChallenge", mock.Anything, &pb.RpcAccountLocalLinkNewChallengeRequest{
AppName: mockedAppName,
Scope: model.AccountAuth_JsonAPI,
}).
Return(&pb.RpcAccountLocalLinkNewChallengeResponse{
Error: &pb.RpcAccountLocalLinkNewChallengeResponseError{Code: pb.RpcAccountLocalLinkNewChallengeResponseError_UNKNOWN_ERROR},
}).Once()
// when
challengeId, err := fx.NewChallenge(ctx, mockedAppName)
// then
require.Error(t, err)
require.Equal(t, ErrFailedGenerateChallenge, err)
require.Empty(t, challengeId)
})
}
func TestAuthService_SolveChallengeForToken(t *testing.T) {
t.Run("successful token retrieval", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("AccountLocalLinkSolveChallenge", mock.Anything, &pb.RpcAccountLocalLinkSolveChallengeRequest{
ChallengeId: mockedChallengeId,
Answer: mockedCode,
}).
Return(&pb.RpcAccountLocalLinkSolveChallengeResponse{
SessionToken: mockedSessionToken,
AppKey: mockedAppKey,
Error: &pb.RpcAccountLocalLinkSolveChallengeResponseError{Code: pb.RpcAccountLocalLinkSolveChallengeResponseError_NULL},
}).Once()
// when
sessionToken, appKey, err := fx.SolveChallenge(ctx, mockedChallengeId, mockedCode)
// then
require.NoError(t, err)
require.Equal(t, mockedSessionToken, sessionToken)
require.Equal(t, mockedAppKey, appKey)
})
t.Run("bad request - missing challenge id or code", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
// when
sessionToken, appKey, err := fx.SolveChallenge(ctx, "", "")
// then
require.Error(t, err)
require.Equal(t, ErrInvalidInput, err)
require.Empty(t, sessionToken)
require.Empty(t, appKey)
})
t.Run("failed token retrieval", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("AccountLocalLinkSolveChallenge", mock.Anything, &pb.RpcAccountLocalLinkSolveChallengeRequest{
ChallengeId: mockedChallengeId,
Answer: mockedCode,
}).
Return(&pb.RpcAccountLocalLinkSolveChallengeResponse{
Error: &pb.RpcAccountLocalLinkSolveChallengeResponseError{Code: pb.RpcAccountLocalLinkSolveChallengeResponseError_UNKNOWN_ERROR},
}).Once()
// when
sessionToken, appKey, err := fx.SolveChallenge(ctx, mockedChallengeId, mockedCode)
// then
require.Error(t, err)
require.Equal(t, ErrFailedAuthenticate, err)
require.Empty(t, sessionToken)
require.Empty(t, appKey)
})
}

View file

@ -0,0 +1,49 @@
package export
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/anyproto/anytype-heart/core/api/util"
)
// GetObjectExportHandler exports an object in specified format
//
// @Summary Export object
// @Tags export
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object_id path string true "Object ID"
// @Param format path string true "Export format" Enums(markdown,protobuf)
// @Success 200 {object} ObjectExportResponse "Object exported successfully"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/objects/{object_id}/export/{format} [post]
func GetObjectExportHandler(s *ExportService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
objectId := c.Param("object_id")
format := c.Query("format")
objectAsRequest := ObjectExportRequest{}
if err := c.ShouldBindJSON(&objectAsRequest); err != nil {
apiErr := util.CodeToAPIError(http.StatusBadRequest, ErrBadInput.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
outputPath, err := s.GetObjectExport(c.Request.Context(), spaceId, objectId, format, objectAsRequest.Path)
code := util.MapErrorCode(err, util.ErrToCode(ErrFailedExportObjectAsMarkdown, http.StatusInternalServerError))
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, ObjectExportResponse{Path: outputPath})
}
}

View file

@ -0,0 +1,9 @@
package export
type ObjectExportRequest struct {
Path string `json:"path" example:"/path/to/export"`
}
type ObjectExportResponse struct {
Path string `json:"path" example:"/path/to/export"`
}

View file

@ -0,0 +1,62 @@
package export
import (
"context"
"errors"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
var (
ErrFailedExportObjectAsMarkdown = errors.New("failed to export object as markdown")
ErrBadInput = errors.New("bad input")
)
type Service interface {
GetObjectExport(ctx context.Context, spaceId string, objectId string, format string, path string) (string, error)
}
type ExportService struct {
mw service.ClientCommandsServer
AccountInfo *model.AccountInfo
}
func NewService(mw service.ClientCommandsServer) *ExportService {
return &ExportService{mw: mw}
}
// GetObjectExport retrieves an object from a space and exports it as a specific format.
func (s *ExportService) GetObjectExport(ctx context.Context, spaceId string, objectId string, format string, path string) (string, error) {
resp := s.mw.ObjectListExport(ctx, &pb.RpcObjectListExportRequest{
SpaceId: spaceId,
Path: path,
ObjectIds: []string{objectId},
Format: s.mapStringToFormat(format),
Zip: false,
IncludeNested: false,
IncludeFiles: true,
IsJson: false,
IncludeArchived: false,
NoProgress: true,
})
if resp.Error.Code != pb.RpcObjectListExportResponseError_NULL {
return "", ErrFailedExportObjectAsMarkdown
}
return resp.Path, nil
}
// mapStringToFormat maps a format string to an ExportFormat enum.
func (s *ExportService) mapStringToFormat(format string) model.ExportFormat {
switch format {
case "markdown":
return model.Export_Markdown
case "protobuf":
return model.Export_Protobuf
default:
return model.Export_Markdown
}
}

View file

@ -0,0 +1,134 @@
package export
import (
"context"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service/mock_service"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
const (
spaceID = "space-123"
objectID = "obj-456"
exportFormat = "markdown"
unrecognizedFormat = "unrecognized"
exportPath = "/some/dir/myexport"
)
type fixture struct {
*ExportService
mwMock *mock_service.MockClientCommandsServer
}
func newFixture(t *testing.T) *fixture {
mw := mock_service.NewMockClientCommandsServer(t)
exportService := NewService(mw)
return &fixture{
ExportService: exportService,
mwMock: mw,
}
}
func TestExportService_GetObjectExport(t *testing.T) {
t.Run("successful export to markdown", func(t *testing.T) {
// Given
ctx := context.Background()
fx := newFixture(t)
// Mock the ObjectListExport call
fx.mwMock.
On("ObjectListExport", mock.Anything, &pb.RpcObjectListExportRequest{
SpaceId: spaceID,
Path: exportPath,
ObjectIds: []string{objectID},
Format: model.Export_Markdown,
Zip: false,
IncludeNested: false,
IncludeFiles: true,
IsJson: false,
IncludeArchived: false,
NoProgress: true,
}).
Return(&pb.RpcObjectListExportResponse{
Path: exportPath,
Error: &pb.RpcObjectListExportResponseError{
Code: pb.RpcObjectListExportResponseError_NULL,
},
}).
Once()
// When
gotPath, err := fx.GetObjectExport(ctx, spaceID, objectID, exportFormat, exportPath)
// Then
require.NoError(t, err)
require.Equal(t, exportPath, gotPath)
fx.mwMock.AssertExpectations(t)
})
t.Run("failed export returns error", func(t *testing.T) {
// Given
ctx := context.Background()
fx := newFixture(t)
// Mock the ObjectListExport call to return an error code
fx.mwMock.
On("ObjectListExport", mock.Anything, mock.Anything).
Return(&pb.RpcObjectListExportResponse{
Path: "",
Error: &pb.RpcObjectListExportResponseError{
Code: pb.RpcObjectListExportResponseError_UNKNOWN_ERROR,
},
}).
Once()
// When
gotPath, err := fx.GetObjectExport(ctx, spaceID, objectID, exportFormat, exportPath)
// Then
require.Error(t, err)
require.Empty(t, gotPath)
require.ErrorIs(t, err, ErrFailedExportObjectAsMarkdown)
fx.mwMock.AssertExpectations(t)
})
t.Run("unrecognized format defaults to markdown", func(t *testing.T) {
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.
On("ObjectListExport", mock.Anything, &pb.RpcObjectListExportRequest{
SpaceId: spaceID,
Path: exportPath,
ObjectIds: []string{objectID},
Format: model.Export_Markdown, // fallback
Zip: false,
IncludeNested: false,
IncludeFiles: true,
IsJson: false,
IncludeArchived: false,
NoProgress: true,
}).
Return(&pb.RpcObjectListExportResponse{
Path: exportPath,
Error: &pb.RpcObjectListExportResponseError{
Code: pb.RpcObjectListExportResponseError_NULL,
},
}).
Once()
// When
gotPath, err := fx.GetObjectExport(ctx, spaceID, objectID, unrecognizedFormat, exportPath) //
// Then
require.NoError(t, err)
require.Equal(t, exportPath, gotPath)
fx.mwMock.AssertExpectations(t)
})
}

View file

@ -0,0 +1,303 @@
package object
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/core/api/util"
)
// GetObjectsHandler retrieves a list of objects in a space
//
// @Summary List objects
// @Tags objects
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Success 200 {object} pagination.PaginatedResponse[Object] "List of objects"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/objects [get]
func GetObjectsHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
offset := c.GetInt("offset")
limit := c.GetInt("limit")
objects, total, hasMore, err := s.ListObjects(c.Request.Context(), spaceId, offset, limit)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedRetrieveObjects, http.StatusInternalServerError),
util.ErrToCode(ErrObjectNotFound, http.StatusInternalServerError),
util.ErrToCode(ErrFailedRetrieveObject, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
pagination.RespondWithPagination(c, http.StatusOK, objects, total, offset, limit, hasMore)
}
}
// GetObjectHandler retrieves an object in a space
//
// @Summary Get object
// @Tags objects
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object_id path string true "Object ID"
// @Success 200 {object} ObjectResponse "The requested object"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/objects/{object_id} [get]
func GetObjectHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
objectId := c.Param("object_id")
object, err := s.GetObject(c.Request.Context(), spaceId, objectId)
code := util.MapErrorCode(err,
util.ErrToCode(ErrObjectNotFound, http.StatusNotFound),
util.ErrToCode(ErrFailedRetrieveObject, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, ObjectResponse{Object: object})
}
}
// DeleteObjectHandler deletes an object in a space
//
// @Summary Delete object
// @Tags objects
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object_id path string true "Object ID"
// @Success 200 {object} ObjectResponse "The deleted object"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 403 {object} util.ForbiddenError "Forbidden"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/objects/{object_id} [delete]
func DeleteObjectHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
objectId := c.Param("object_id")
object, err := s.DeleteObject(c.Request.Context(), spaceId, objectId)
code := util.MapErrorCode(err,
util.ErrToCode(ErrObjectNotFound, http.StatusNotFound),
util.ErrToCode(ErrFailedDeleteObject, http.StatusForbidden),
util.ErrToCode(ErrFailedRetrieveObject, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, ObjectResponse{Object: object})
}
}
// CreateObjectHandler creates a new object in a space
//
// @Summary Create object
// @Tags objects
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param object body CreateObjectRequest true "Object to create"
// @Success 200 {object} ObjectResponse "The created object"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/objects [post]
func CreateObjectHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
request := CreateObjectRequest{}
if err := c.BindJSON(&request); err != nil {
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
object, err := s.CreateObject(c.Request.Context(), spaceId, request)
code := util.MapErrorCode(err,
util.ErrToCode(ErrInputMissingSource, http.StatusBadRequest),
util.ErrToCode(ErrFailedCreateObject, http.StatusInternalServerError),
util.ErrToCode(ErrFailedSetRelationFeatured, http.StatusInternalServerError),
util.ErrToCode(ErrFailedFetchBookmark, http.StatusInternalServerError),
util.ErrToCode(ErrObjectNotFound, http.StatusInternalServerError),
util.ErrToCode(ErrFailedRetrieveObject, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, ObjectResponse{Object: object})
}
}
// GetTypesHandler retrieves a list of types in a space
//
// @Summary List types
// @Tags types
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Success 200 {object} pagination.PaginatedResponse[Type] "List of types"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/types [get]
func GetTypesHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
offset := c.GetInt("offset")
limit := c.GetInt("limit")
types, total, hasMore, err := s.ListTypes(c.Request.Context(), spaceId, offset, limit)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedRetrieveTypes, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
pagination.RespondWithPagination(c, http.StatusOK, types, total, offset, limit, hasMore)
}
}
// GetTypeHandler retrieves a type in a space
//
// @Summary Get type
// @Tags types
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param type_id path string true "Type ID"
// @Success 200 {object} TypeResponse "The requested type"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/types/{type_id} [get]
func GetTypeHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
typeId := c.Param("type_id")
object, err := s.GetType(c.Request.Context(), spaceId, typeId)
code := util.MapErrorCode(err,
util.ErrToCode(ErrTypeNotFound, http.StatusNotFound),
util.ErrToCode(ErrFailedRetrieveType, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, TypeResponse{Type: object})
}
}
// GetTemplatesHandler retrieves a list of templates for a type in a space
//
// @Summary List templates
// @Tags types
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param type_id path string true "Type ID"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Success 200 {object} pagination.PaginatedResponse[Template] "List of templates"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/types/{type_id}/templates [get]
func GetTemplatesHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
typeId := c.Param("type_id")
offset := c.GetInt("offset")
limit := c.GetInt("limit")
templates, total, hasMore, err := s.ListTemplates(c.Request.Context(), spaceId, typeId, offset, limit)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedRetrieveTemplateType, http.StatusInternalServerError),
util.ErrToCode(ErrTemplateTypeNotFound, http.StatusInternalServerError),
util.ErrToCode(ErrFailedRetrieveTemplates, http.StatusInternalServerError),
util.ErrToCode(ErrFailedRetrieveTemplate, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
pagination.RespondWithPagination(c, http.StatusOK, templates, total, offset, limit, hasMore)
}
}
// GetTemplateHandler retrieves a template for a type in a space
//
// @Summary Get template
// @Tags types
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param type_id path string true "Type ID"
// @Param template_id path string true "Template ID"
// @Success 200 {object} TemplateResponse "The requested template"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 404 {object} util.NotFoundError "Resource not found"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/types/{type_id}/templates/{template_id} [get]
func GetTemplateHandler(s *ObjectService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
typeId := c.Param("type_id")
templateId := c.Param("template_id")
object, err := s.GetTemplate(c.Request.Context(), spaceId, typeId, templateId)
code := util.MapErrorCode(err,
util.ErrToCode(ErrTemplateNotFound, http.StatusNotFound),
util.ErrToCode(ErrFailedRetrieveTemplate, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, TemplateResponse{Template: object})
}
}

View file

@ -0,0 +1,93 @@
package object
type CreateObjectRequest struct {
Name string `json:"name" example:"Object Name"`
Icon string `json:"icon" example:"📄"`
Description string `json:"description" example:"Object Description"`
Body string `json:"body" example:"Object Body"`
Source string `json:"source" example:"https://source.com"`
TemplateId string `json:"template_id" example:"bafyreictrp3obmnf6dwejy5o4p7bderaaia4bdg2psxbfzf44yya5uutge"`
ObjectTypeUniqueKey string `json:"object_type_unique_key" example:"ot-page"`
}
type ObjectResponse struct {
Object Object `json:"object"`
}
type Object struct {
Type string `json:"type" example:"Page"`
Id string `json:"id" example:"bafyreie6n5l5nkbjal37su54cha4coy7qzuhrnajluzv5qd5jvtsrxkequ"`
Name string `json:"name" example:"Object Name"`
Icon string `json:"icon" example:"📄"`
Snippet string `json:"snippet" example:"The beginning of the object body..."`
Layout string `json:"layout" example:"basic"`
SpaceId string `json:"space_id" example:"bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1"`
RootId string `json:"root_id" example:"bafyreicypzj6uvu54664ucv3hmbsd5cmdy2dv4fwua26sciq74khzpyn4u"`
Blocks []Block `json:"blocks"`
Details []Detail `json:"details"`
}
type Block struct {
Id string `json:"id" example:"64394517de52ad5acb89c66c"`
ChildrenIds []string `json:"children_ids" example:"['6797ce8ecda913cde14b02dc']"`
BackgroundColor string `json:"background_color" example:"red"`
Align string `json:"align" enums:"AlignLeft,AlignCenter,AlignRight,AlignJustify" example:"AlignLeft"`
VerticalAlign string `json:"vertical_align" enums:"VerticalAlignTop,VerticalAlignMiddle,VerticalAlignBottom" example:"VerticalAlignTop"`
Text *Text `json:"text,omitempty"`
File *File `json:"file,omitempty"`
}
type Text struct {
Text string `json:"text" example:"Some text"`
Style string `json:"style" enums:"Paragraph,Header1,Header2,Header3,Header4,Quote,Code,Title,Checkbox,Marked,Numbered,Toggle,Description,Callout" example:"Paragraph"`
Checked bool `json:"checked" example:"true"`
Color string `json:"color" example:"red"`
Icon string `json:"icon" example:"📄"`
}
type File struct {
Hash string `json:"hash"`
Name string `json:"name"`
Type string `json:"type"`
Mime string `json:"mime"`
Size int `json:"size"`
AddedAt int `json:"added_at"`
TargetObjectId string `json:"target_object_id"`
State string `json:"state"`
Style string `json:"style"`
}
type Detail struct {
Id string `json:"id" enums:"last_modified_date,last_modified_by,created_date,created_by,last_opened_date,tags" example:"last_modified_date"`
Details map[string]interface{} `json:"details"`
}
type Tag struct {
Id string `json:"id" example:"bafyreiaixlnaefu3ci22zdenjhsdlyaeeoyjrsid5qhfeejzlccijbj7sq"`
Name string `json:"name" example:"Tag Name"`
Color string `json:"color" example:"yellow"`
}
type TypeResponse struct {
Type Type `json:"type"`
}
type Type struct {
Type string `json:"type" example:"type"`
Id string `json:"id" example:"bafyreigyb6l5szohs32ts26ku2j42yd65e6hqy2u3gtzgdwqv6hzftsetu"`
UniqueKey string `json:"unique_key" example:"ot-page"`
Name string `json:"name" example:"Page"`
Icon string `json:"icon" example:"📄"`
RecommendedLayout string `json:"recommended_layout" example:"todo"`
}
type TemplateResponse struct {
Template Template `json:"template"`
}
type Template struct {
Type string `json:"type" example:"template"`
Id string `json:"id" example:"bafyreictrp3obmnf6dwejy5o4p7bderaaia4bdg2psxbfzf44yya5uutge"`
Name string `json:"name" example:"Template Name"`
Icon string `json:"icon" example:"📄"`
}

View file

@ -0,0 +1,564 @@
package object
import (
"context"
"errors"
"time"
"github.com/gogo/protobuf/types"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
var (
// objects
ErrObjectNotFound = errors.New("object not found")
ErrFailedRetrieveObject = errors.New("failed to retrieve object")
ErrFailedRetrieveObjects = errors.New("failed to retrieve list of objects")
ErrFailedDeleteObject = errors.New("failed to delete object")
ErrFailedCreateObject = errors.New("failed to create object")
ErrInputMissingSource = errors.New("source is missing for bookmark")
ErrFailedSetRelationFeatured = errors.New("failed to set relation featured")
ErrFailedFetchBookmark = errors.New("failed to fetch bookmark")
ErrFailedPasteBody = errors.New("failed to paste body")
// types
ErrFailedRetrieveTypes = errors.New("failed to retrieve types")
ErrTypeNotFound = errors.New("type not found")
ErrFailedRetrieveType = errors.New("failed to retrieve type")
ErrFailedRetrieveTemplateType = errors.New("failed to retrieve template type")
ErrTemplateTypeNotFound = errors.New("template type not found")
ErrFailedRetrieveTemplate = errors.New("failed to retrieve template")
ErrFailedRetrieveTemplates = errors.New("failed to retrieve templates")
ErrTemplateNotFound = errors.New("template not found")
)
type Service interface {
ListObjects(ctx context.Context, spaceId string, offset int, limit int) ([]Object, int, bool, error)
GetObject(ctx context.Context, spaceId string, objectId string) (Object, error)
DeleteObject(ctx context.Context, spaceId string, objectId string) (Object, error)
CreateObject(ctx context.Context, spaceId string, request CreateObjectRequest) (Object, error)
ListTypes(ctx context.Context, spaceId string, offset int, limit int) ([]Type, int, bool, error)
GetType(ctx context.Context, spaceId string, typeId string) (Type, error)
ListTemplates(ctx context.Context, spaceId string, typeId string, offset int, limit int) ([]Template, int, bool, error)
GetTemplate(ctx context.Context, spaceId string, typeId string, templateId string) (Template, error)
}
type ObjectService struct {
mw service.ClientCommandsServer
spaceService *space.SpaceService
AccountInfo *model.AccountInfo
}
func NewService(mw service.ClientCommandsServer, spaceService *space.SpaceService) *ObjectService {
return &ObjectService{mw: mw, spaceService: spaceService}
}
// ListObjects retrieves a paginated list of objects in a specific space.
func (s *ObjectService) ListObjects(ctx context.Context, spaceId string, offset int, limit int) (objects []Object, total int, hasMore bool, err error) {
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyLayout.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.IntList([]int{
int(model.ObjectType_basic),
int(model.ObjectType_profile),
int(model.ObjectType_todo),
int(model.ObjectType_note),
int(model.ObjectType_bookmark),
int(model.ObjectType_set),
int(model.ObjectType_collection),
int(model.ObjectType_participant),
}...),
},
},
Sorts: []*model.BlockContentDataviewSort{{
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
Type: model.BlockContentDataviewSort_Desc,
Format: model.RelationFormat_longtext,
IncludeTime: true,
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
}},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return nil, 0, false, ErrFailedRetrieveObjects
}
total = len(resp.Records)
paginatedObjects, hasMore := pagination.Paginate(resp.Records, offset, limit)
objects = make([]Object, 0, len(paginatedObjects))
for _, record := range paginatedObjects {
object, err := s.GetObject(ctx, spaceId, record.Fields[bundle.RelationKeyId.String()].GetStringValue())
if err != nil {
return nil, 0, false, err
}
objects = append(objects, object)
}
return objects, total, hasMore, nil
}
// GetObject retrieves a single object by its ID in a specific space.
func (s *ObjectService) GetObject(ctx context.Context, spaceId string, objectId string) (Object, error) {
resp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
SpaceId: spaceId,
ObjectId: objectId,
})
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
return Object{}, ErrObjectNotFound
}
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return Object{}, ErrFailedRetrieveObject
}
icon := util.GetIconFromEmojiOrImage(s.AccountInfo, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconImage.String()].GetStringValue())
objectTypeName, err := util.ResolveTypeToName(s.mw, spaceId, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyType.String()].GetStringValue())
if err != nil {
return Object{}, err
}
object := Object{
Type: objectTypeName,
Id: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyId.String()].GetStringValue(),
Name: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: icon,
Snippet: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySnippet.String()].GetStringValue(),
Layout: model.ObjectTypeLayout_name[int32(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyLayout.String()].GetNumberValue())],
SpaceId: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(),
RootId: resp.ObjectView.RootId,
Blocks: s.GetBlocks(resp),
Details: s.GetDetails(resp),
}
return object, nil
}
// DeleteObject deletes an existing object in a specific space.
func (s *ObjectService) DeleteObject(ctx context.Context, spaceId string, objectId string) (Object, error) {
object, err := s.GetObject(ctx, spaceId, objectId)
if err != nil {
return Object{}, err
}
resp := s.mw.ObjectSetIsArchived(ctx, &pb.RpcObjectSetIsArchivedRequest{
ContextId: objectId,
IsArchived: true,
})
if resp.Error.Code != pb.RpcObjectSetIsArchivedResponseError_NULL {
return Object{}, ErrFailedDeleteObject
}
return object, nil
}
// CreateObject creates a new object in a specific space.
func (s *ObjectService) CreateObject(ctx context.Context, spaceId string, request CreateObjectRequest) (Object, error) {
if request.ObjectTypeUniqueKey == "ot-bookmark" && request.Source == "" {
return Object{}, ErrInputMissingSource
}
details := &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(request.Name),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(request.Icon),
bundle.RelationKeyDescription.String(): pbtypes.String(request.Description),
bundle.RelationKeySource.String(): pbtypes.String(request.Source),
bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_api)),
},
}
resp := s.mw.ObjectCreate(ctx, &pb.RpcObjectCreateRequest{
Details: details,
TemplateId: request.TemplateId,
SpaceId: spaceId,
ObjectTypeUniqueKey: request.ObjectTypeUniqueKey,
WithChat: false,
})
if resp.Error.Code != pb.RpcObjectCreateResponseError_NULL {
return Object{}, ErrFailedCreateObject
}
// ObjectRelationAddFeatured if description was set
if request.Description != "" {
relAddFeatResp := s.mw.ObjectRelationAddFeatured(ctx, &pb.RpcObjectRelationAddFeaturedRequest{
ContextId: resp.ObjectId,
Relations: []string{bundle.RelationKeyDescription.String()},
})
if relAddFeatResp.Error.Code != pb.RpcObjectRelationAddFeaturedResponseError_NULL {
object, _ := s.GetObject(ctx, spaceId, resp.ObjectId) // nolint:errcheck
return object, ErrFailedSetRelationFeatured
}
}
// ObjectBookmarkFetch after creating a bookmark object
if request.ObjectTypeUniqueKey == "ot-bookmark" {
bookmarkResp := s.mw.ObjectBookmarkFetch(ctx, &pb.RpcObjectBookmarkFetchRequest{
ContextId: resp.ObjectId,
Url: request.Source,
})
if bookmarkResp.Error.Code != pb.RpcObjectBookmarkFetchResponseError_NULL {
object, _ := s.GetObject(ctx, spaceId, resp.ObjectId) // nolint:errcheck
return object, ErrFailedFetchBookmark
}
}
// First call BlockCreate at top, then BlockPaste to paste the body
if request.Body != "" {
blockCreateResp := s.mw.BlockCreate(ctx, &pb.RpcBlockCreateRequest{
ContextId: resp.ObjectId,
TargetId: "",
Block: &model.Block{
Id: "",
BackgroundColor: "",
Align: model.Block_AlignLeft,
VerticalAlign: model.Block_VerticalAlignTop,
Content: &model.BlockContentOfText{
Text: &model.BlockContentText{
Text: "",
Style: model.BlockContentText_Paragraph,
Checked: false,
Color: "",
IconEmoji: "",
IconImage: "",
},
},
},
Position: model.Block_Bottom,
})
if blockCreateResp.Error.Code != pb.RpcBlockCreateResponseError_NULL {
object, _ := s.GetObject(ctx, spaceId, resp.ObjectId) // nolint:errcheck
return object, ErrFailedCreateObject
}
blockPasteResp := s.mw.BlockPaste(ctx, &pb.RpcBlockPasteRequest{
ContextId: resp.ObjectId,
FocusedBlockId: blockCreateResp.BlockId,
TextSlot: request.Body,
})
if blockPasteResp.Error.Code != pb.RpcBlockPasteResponseError_NULL {
object, _ := s.GetObject(ctx, spaceId, resp.ObjectId) // nolint:errcheck
return object, ErrFailedPasteBody
}
}
return s.GetObject(ctx, spaceId, resp.ObjectId)
}
// ListTypes returns a paginated list of types in a specific space.
func (s *ObjectService) ListTypes(ctx context.Context, spaceId string, offset int, limit int) (types []Type, total int, hasMore bool, err error) {
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyLayout.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ObjectType_objectType)),
},
{
RelationKey: bundle.RelationKeyIsHidden.String(),
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.Bool(true),
},
},
Sorts: []*model.BlockContentDataviewSort{
{
RelationKey: bundle.RelationKeyName.String(),
Type: model.BlockContentDataviewSort_Asc,
},
},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyUniqueKey.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyRecommendedLayout.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return nil, 0, false, ErrFailedRetrieveTypes
}
total = len(resp.Records)
paginatedTypes, hasMore := pagination.Paginate(resp.Records, offset, limit)
types = make([]Type, 0, len(paginatedTypes))
for _, record := range paginatedTypes {
types = append(types, Type{
Type: "type",
Id: record.Fields[bundle.RelationKeyId.String()].GetStringValue(),
UniqueKey: record.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue(),
Name: record.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(),
RecommendedLayout: model.ObjectTypeLayout_name[int32(record.Fields[bundle.RelationKeyRecommendedLayout.String()].GetNumberValue())],
})
}
return types, total, hasMore, nil
}
// GetType returns a single type by its ID in a specific space.
func (s *ObjectService) GetType(ctx context.Context, spaceId string, typeId string) (Type, error) {
resp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
SpaceId: spaceId,
ObjectId: typeId,
})
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
return Type{}, ErrTypeNotFound
}
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return Type{}, ErrFailedRetrieveType
}
return Type{
Type: "type",
Id: typeId,
UniqueKey: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue(),
Name: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(),
RecommendedLayout: model.ObjectTypeLayout_name[int32(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyRecommendedLayout.String()].GetNumberValue())],
}, nil
}
// ListTemplates returns a paginated list of templates in a specific space.
func (s *ObjectService) ListTemplates(ctx context.Context, spaceId string, typeId string, offset int, limit int) (templates []Template, total int, hasMore bool, err error) {
// First, determine the type ID of "ot-template" in the space
templateTypeIdResp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyUniqueKey.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String("ot-template"),
},
},
Keys: []string{bundle.RelationKeyId.String()},
})
if templateTypeIdResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return nil, 0, false, ErrFailedRetrieveTemplateType
}
if len(templateTypeIdResp.Records) == 0 {
return nil, 0, false, ErrTemplateTypeNotFound
}
// Then, search all objects of the template type and filter by the target object type
templateTypeId := templateTypeIdResp.Records[0].Fields[bundle.RelationKeyId.String()].GetStringValue()
templateObjectsResp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyType.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(templateTypeId),
},
},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyTargetObjectType.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String()},
})
if templateObjectsResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return nil, 0, false, ErrFailedRetrieveTemplates
}
templateIds := make([]string, 0)
for _, record := range templateObjectsResp.Records {
if record.Fields[bundle.RelationKeyTargetObjectType.String()].GetStringValue() == typeId {
templateIds = append(templateIds, record.Fields[bundle.RelationKeyId.String()].GetStringValue())
}
}
total = len(templateIds)
paginatedTemplates, hasMore := pagination.Paginate(templateIds, offset, limit)
templates = make([]Template, 0, len(paginatedTemplates))
// Finally, open each template and populate the response
for _, templateId := range paginatedTemplates {
templateResp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
SpaceId: spaceId,
ObjectId: templateId,
})
if templateResp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return nil, 0, false, ErrFailedRetrieveTemplate
}
templates = append(templates, Template{
Type: "template",
Id: templateId,
Name: templateResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: templateResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(),
})
}
return templates, total, hasMore, nil
}
// GetTemplate returns a single template by its ID in a specific space.
func (s *ObjectService) GetTemplate(ctx context.Context, spaceId string, typeId string, templateId string) (Template, error) {
resp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
SpaceId: spaceId,
ObjectId: templateId,
})
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
return Template{}, ErrTemplateNotFound
}
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
return Template{}, ErrFailedRetrieveTemplate
}
return Template{
Type: "template",
Id: templateId,
Name: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(),
}, nil
}
// GetDetails returns the list of details from the ObjectShowResponse.
func (s *ObjectService) GetDetails(resp *pb.RpcObjectShowResponse) []Detail {
creator := resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyCreator.String()].GetStringValue()
lastModifiedBy := resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyLastModifiedBy.String()].GetStringValue()
var creatorId, lastModifiedById string
for _, detail := range resp.ObjectView.Details {
if detail.Id == creator {
creatorId = detail.Id
}
if detail.Id == lastModifiedBy {
lastModifiedById = detail.Id
}
}
return []Detail{
{
Id: "last_modified_date",
Details: map[string]interface{}{
"last_modified_date": PosixToISO8601(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyLastModifiedDate.String()].GetNumberValue()),
},
},
{
Id: "last_modified_by",
Details: map[string]interface{}{
"details": s.spaceService.GetParticipantDetails(s.mw, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(), lastModifiedById),
},
},
{
Id: "created_date",
Details: map[string]interface{}{
"created_date": PosixToISO8601(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyCreatedDate.String()].GetNumberValue()),
},
},
{
Id: "created_by",
Details: map[string]interface{}{
"details": s.spaceService.GetParticipantDetails(s.mw, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(), creatorId),
},
},
{
Id: "last_opened_date",
Details: map[string]interface{}{
"last_opened_date": PosixToISO8601(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyLastOpenedDate.String()].GetNumberValue()),
},
},
{
Id: "tags",
Details: map[string]interface{}{
"tags": s.getTags(resp),
},
},
}
}
// getTags returns the list of tags from the ObjectShowResponse
func (s *ObjectService) getTags(resp *pb.RpcObjectShowResponse) []Tag {
tags := []Tag{}
tagField, ok := resp.ObjectView.Details[0].Details.Fields["tag"]
if !ok || tagField.GetListValue() == nil {
return tags
}
for _, tagId := range tagField.GetListValue().Values {
id := tagId.GetStringValue()
for _, detail := range resp.ObjectView.Details {
if detail.Id == id {
tags = append(tags, Tag{
Id: id,
Name: detail.Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Color: detail.Details.Fields[bundle.RelationKeyRelationOptionColor.String()].GetStringValue(),
})
break
}
}
}
return tags
}
// GetBlocks returns the list of blocks from the ObjectShowResponse.
func (s *ObjectService) GetBlocks(resp *pb.RpcObjectShowResponse) []Block {
blocks := []Block{}
for _, block := range resp.ObjectView.Blocks {
var text *Text
var file *File
switch content := block.Content.(type) {
case *model.BlockContentOfText:
text = &Text{
Text: content.Text.Text,
Style: model.BlockContentTextStyle_name[int32(content.Text.Style)],
Checked: content.Text.Checked,
Color: content.Text.Color,
Icon: util.GetIconFromEmojiOrImage(s.AccountInfo, content.Text.IconEmoji, content.Text.IconImage),
}
case *model.BlockContentOfFile:
file = &File{
Hash: content.File.Hash,
Name: content.File.Name,
Type: model.BlockContentFileType_name[int32(content.File.Type)],
Mime: content.File.Mime,
Size: content.File.Size(),
AddedAt: int(content.File.AddedAt),
TargetObjectId: content.File.TargetObjectId,
State: model.BlockContentFileState_name[int32(content.File.State)],
Style: model.BlockContentFileStyle_name[int32(content.File.Style)],
}
// TODO: other content types?
}
blocks = append(blocks, Block{
Id: block.Id,
ChildrenIds: block.ChildrenIds,
BackgroundColor: block.BackgroundColor,
Align: model.BlockAlign_name[int32(block.Align)],
VerticalAlign: model.BlockVerticalAlign_name[int32(block.VerticalAlign)],
Text: text,
File: file,
})
}
return blocks
}
func PosixToISO8601(posix float64) string {
t := time.Unix(int64(posix), 0).UTC()
return t.Format(time.RFC3339)
}

View file

@ -0,0 +1,781 @@
package object
import (
"context"
"testing"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service/mock_service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
const (
offset = 0
limit = 100
mockedTechSpaceId = "mocked-tech-space-id"
gatewayUrl = "http://localhost:31006"
mockedSpaceId = "mocked-space-id"
mockedObjectId = "mocked-object-id"
mockedObjectType = "mocked-object-type"
mockedNewObjectId = "mocked-new-object-id"
mockedObjectName = "mocked-object-name"
mockedObjectSnippet = "mocked-object-snippet"
mockedObjectIcon = "🔍"
mockedObjectTypeUniqueKey = "ot-page"
mockedTypeId = "mocked-type-id"
mockedTypeName = "mocked-type-name"
mockedTypeUniqueKey = "mocked-type-unique-key"
mockedTypeIcon = "📝"
mockedTemplateId = "mocked-template-id"
mockedTemplateName = "mocked-template-name"
mockedTemplateIcon = "📃"
)
type fixture struct {
*ObjectService
mwMock *mock_service.MockClientCommandsServer
}
func newFixture(t *testing.T) *fixture {
mw := mock_service.NewMockClientCommandsServer(t)
spaceService := space.NewService(mw)
objectService := NewService(mw, spaceService)
objectService.AccountInfo = &model.AccountInfo{
TechSpaceId: mockedTechSpaceId,
GatewayUrl: gatewayUrl,
}
return &fixture{
ObjectService: objectService,
mwMock: mw,
}
}
func TestObjectService_ListObjects(t *testing.T) {
t.Run("successfully get objects for a space", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyLayout.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.IntList([]int{
int(model.ObjectType_basic),
int(model.ObjectType_profile),
int(model.ObjectType_todo),
int(model.ObjectType_note),
int(model.ObjectType_bookmark),
int(model.ObjectType_set),
int(model.ObjectType_collection),
int(model.ObjectType_participant),
}...),
},
},
Sorts: []*model.BlockContentDataviewSort{{
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
Type: model.BlockContentDataviewSort_Desc,
Format: model.RelationFormat_longtext,
IncludeTime: true,
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
}},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String()},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeySnippet.String(): pbtypes.String(mockedObjectSnippet),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
bundle.RelationKeyType.String(): pbtypes.String(mockedObjectTypeUniqueKey),
bundle.RelationKeyLayout.String(): pbtypes.Float64(float64(model.ObjectType_basic)),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock object show for object details
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedObjectId,
}).Return(&pb.RpcObjectShowResponse{
ObjectView: &model.ObjectView{
RootId: mockedObjectId,
Details: []*model.ObjectViewDetailsSet{
{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeySnippet.String(): pbtypes.String(mockedObjectSnippet),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
bundle.RelationKeyType.String(): pbtypes.String(mockedObjectTypeUniqueKey),
bundle.RelationKeyCreatedDate.String(): pbtypes.Float64(888888),
bundle.RelationKeyLastModifiedDate.String(): pbtypes.Float64(999999),
bundle.RelationKeyLastOpenedDate.String(): pbtypes.Float64(0),
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
},
},
},
},
},
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
}).Once()
// Mock type resolution
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyUniqueKey.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(mockedObjectTypeUniqueKey),
},
},
Keys: []string{bundle.RelationKeyName.String()},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectType),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock participant details
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(""),
},
},
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyIconEmoji.String(),
bundle.RelationKeyIconImage.String(),
bundle.RelationKeyIdentity.String(),
bundle.RelationKeyGlobalName.String(),
bundle.RelationKeyParticipantPermissions.String(),
},
}).Return(&pb.RpcObjectSearchResponse{
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
Records: []*types.Struct{
{},
},
}).Twice()
// when
objects, total, hasMore, err := fx.ListObjects(ctx, mockedSpaceId, offset, limit)
// then
require.NoError(t, err)
require.Len(t, objects, 1)
require.Equal(t, mockedObjectType, objects[0].Type)
require.Equal(t, mockedObjectId, objects[0].Id)
require.Equal(t, mockedObjectName, objects[0].Name)
require.Equal(t, mockedObjectSnippet, objects[0].Snippet)
require.Equal(t, mockedObjectIcon, objects[0].Icon)
require.Equal(t, 6, len(objects[0].Details))
for _, detail := range objects[0].Details {
if detail.Id == "created_date" {
require.Equal(t, "1970-01-11T06:54:48Z", detail.Details["created_date"])
} else if detail.Id == "created_by" {
require.Empty(t, detail.Details["created_by"])
} else if detail.Id == "last_modified_date" {
require.Equal(t, "1970-01-12T13:46:39Z", detail.Details["last_modified_date"])
} else if detail.Id == "last_modified_by" {
require.Empty(t, detail.Details["last_modified_by"])
} else if detail.Id == "last_opened_date" {
require.Equal(t, "1970-01-01T00:00:00Z", detail.Details["last_opened_date"])
} else if detail.Id == "tags" {
require.Empty(t, detail.Details["tags"])
} else {
t.Errorf("unexpected detail id: %s", detail.Id)
}
}
require.Equal(t, 1, total)
require.False(t, hasMore)
})
t.Run("no objects found", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// when
objects, total, hasMore, err := fx.ListObjects(ctx, "empty-space", offset, limit)
// then
require.NoError(t, err)
require.Len(t, objects, 0)
require.Equal(t, 0, total)
require.False(t, hasMore)
})
}
func TestObjectService_GetObject(t *testing.T) {
t.Run("object found", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedObjectId,
}).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
RootId: mockedObjectId,
Details: []*model.ObjectViewDetailsSet{
{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeySnippet.String(): pbtypes.String(mockedObjectSnippet),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeyType.String(): pbtypes.String(mockedObjectTypeUniqueKey),
bundle.RelationKeyLastModifiedDate.String(): pbtypes.Float64(999999),
bundle.RelationKeyCreatedDate.String(): pbtypes.Float64(888888),
bundle.RelationKeyLastOpenedDate.String(): pbtypes.Float64(0),
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
},
},
},
},
},
}, nil).Once()
// Mock type resolution
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyUniqueKey.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(mockedObjectTypeUniqueKey),
},
},
Keys: []string{bundle.RelationKeyName.String()},
}).Return(&pb.RpcObjectSearchResponse{
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectType),
},
},
},
}, nil).Once()
// Mock participant details
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(""),
},
},
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyIconEmoji.String(),
bundle.RelationKeyIconImage.String(),
bundle.RelationKeyIdentity.String(),
bundle.RelationKeyGlobalName.String(),
bundle.RelationKeyParticipantPermissions.String(),
},
}).Return(&pb.RpcObjectSearchResponse{
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
Records: []*types.Struct{
{},
},
}).Twice()
// when
object, err := fx.GetObject(ctx, mockedSpaceId, mockedObjectId)
// then
require.NoError(t, err)
require.Equal(t, mockedObjectType, object.Type)
require.Equal(t, mockedObjectId, object.Id)
require.Equal(t, mockedObjectName, object.Name)
require.Equal(t, mockedObjectSnippet, object.Snippet)
require.Equal(t, mockedObjectName, object.Icon)
require.Equal(t, 6, len(object.Details))
for _, detail := range object.Details {
if detail.Id == "created_date" {
require.Equal(t, "1970-01-11T06:54:48Z", detail.Details["created_date"])
} else if detail.Id == "created_by" {
require.Empty(t, detail.Details["created_by"])
} else if detail.Id == "last_modified_date" {
require.Equal(t, "1970-01-12T13:46:39Z", detail.Details["last_modified_date"])
} else if detail.Id == "last_modified_by" {
require.Empty(t, detail.Details["last_modified_by"])
} else if detail.Id == "last_opened_date" {
require.Equal(t, "1970-01-01T00:00:00Z", detail.Details["last_opened_date"])
} else if detail.Id == "tags" {
require.Empty(t, detail.Details["tags"])
} else {
t.Errorf("unexpected detail id: %s", detail.Id)
}
}
})
t.Run("object not found", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectShow", mock.Anything, mock.Anything).
Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NOT_FOUND},
}, nil).Once()
// when
object, err := fx.GetObject(ctx, mockedSpaceId, "missing-obj")
// then
require.ErrorIs(t, err, ErrObjectNotFound)
require.Empty(t, object)
})
}
func TestObjectService_CreateObject(t *testing.T) {
t.Run("successful object creation", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectCreate", mock.Anything, &pb.RpcObjectCreateRequest{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
bundle.RelationKeyDescription.String(): pbtypes.String(""),
bundle.RelationKeySource.String(): pbtypes.String(""),
bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_api)),
},
},
TemplateId: "",
SpaceId: mockedSpaceId,
ObjectTypeUniqueKey: mockedObjectTypeUniqueKey,
WithChat: false,
}).Return(&pb.RpcObjectCreateResponse{
ObjectId: mockedNewObjectId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedNewObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
},
},
Error: &pb.RpcObjectCreateResponseError{Code: pb.RpcObjectCreateResponseError_NULL},
}).Once()
// Mock object show for object details
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedNewObjectId,
}).Return(&pb.RpcObjectShowResponse{
ObjectView: &model.ObjectView{
RootId: mockedNewObjectId,
Details: []*model.ObjectViewDetailsSet{
{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedNewObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeyLayout.String(): pbtypes.Float64(float64(model.ObjectType_basic)),
bundle.RelationKeyType.String(): pbtypes.String(mockedObjectTypeUniqueKey),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
},
},
},
},
},
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
}).Once()
// Mock type resolution
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyUniqueKey.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(mockedObjectTypeUniqueKey),
},
},
Keys: []string{bundle.RelationKeyName.String()},
}).Return(&pb.RpcObjectSearchResponse{
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectType),
},
},
},
}).Once()
// Mock participant details
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(""),
},
},
Keys: []string{
bundle.RelationKeyId.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyIconEmoji.String(),
bundle.RelationKeyIconImage.String(),
bundle.RelationKeyIdentity.String(),
bundle.RelationKeyGlobalName.String(),
bundle.RelationKeyParticipantPermissions.String(),
},
}).Return(&pb.RpcObjectSearchResponse{
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
Records: []*types.Struct{
{},
},
}).Twice()
// when
object, err := fx.CreateObject(ctx, mockedSpaceId, CreateObjectRequest{
Name: mockedObjectName,
Icon: mockedObjectIcon,
// TODO: use actual values
TemplateId: "",
ObjectTypeUniqueKey: mockedObjectTypeUniqueKey,
})
// then
require.NoError(t, err)
require.Equal(t, mockedObjectType, object.Type)
require.Equal(t, mockedNewObjectId, object.Id)
require.Equal(t, mockedObjectName, object.Name)
require.Equal(t, mockedObjectIcon, object.Icon)
require.Equal(t, mockedSpaceId, object.SpaceId)
})
t.Run("creation error", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectCreate", mock.Anything, mock.Anything).
Return(&pb.RpcObjectCreateResponse{
Error: &pb.RpcObjectCreateResponseError{Code: pb.RpcObjectCreateResponseError_UNKNOWN_ERROR},
}).Once()
// when
object, err := fx.CreateObject(ctx, mockedSpaceId, CreateObjectRequest{
Name: "Fail Object",
Icon: "",
})
// then
require.ErrorIs(t, err, ErrFailedCreateObject)
require.Empty(t, object)
})
}
func TestObjectService_ListTypes(t *testing.T) {
t.Run("types found", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("type-1"),
bundle.RelationKeyName.String(): pbtypes.String("Type One"),
bundle.RelationKeyUniqueKey.String(): pbtypes.String("type-one-key"),
bundle.RelationKeyIconEmoji.String(): pbtypes.String("🗂️"),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// when
types, total, hasMore, err := fx.ListTypes(ctx, mockedSpaceId, offset, limit)
// then
require.NoError(t, err)
require.Len(t, types, 1)
require.Equal(t, "type-1", types[0].Id)
require.Equal(t, "Type One", types[0].Name)
require.Equal(t, "type-one-key", types[0].UniqueKey)
require.Equal(t, "🗂️", types[0].Icon)
require.Equal(t, 1, total)
require.False(t, hasMore)
})
t.Run("no types found", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// when
types, total, hasMore, err := fx.ListTypes(ctx, "empty-space", offset, limit)
// then
require.NoError(t, err)
require.Len(t, types, 0)
require.Equal(t, 0, total)
require.False(t, hasMore)
})
}
func TestObjectService_GetType(t *testing.T) {
t.Run("type found", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedTypeId,
}).Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedTypeId),
bundle.RelationKeyName.String(): pbtypes.String(mockedTypeName),
bundle.RelationKeyUniqueKey.String(): pbtypes.String(mockedTypeUniqueKey),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedTypeIcon),
bundle.RelationKeyRecommendedLayout.String(): pbtypes.Float64(float64(model.ObjectType_basic)),
},
},
},
},
},
}).Once()
// when
objType, err := fx.GetType(ctx, mockedSpaceId, mockedTypeId)
// then
require.NoError(t, err)
require.Equal(t, mockedTypeId, objType.Id)
require.Equal(t, mockedTypeName, objType.Name)
require.Equal(t, mockedTypeUniqueKey, objType.UniqueKey)
require.Equal(t, mockedTypeIcon, objType.Icon)
require.Equal(t, model.ObjectTypeLayout_name[int32(model.ObjectType_basic)], objType.RecommendedLayout)
})
t.Run("type not found", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedTypeId,
}).Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NOT_FOUND},
}).Once()
// when
objType, err := fx.GetType(ctx, mockedSpaceId, mockedTypeId)
// then
require.ErrorIs(t, err, ErrTypeNotFound)
require.Empty(t, objType)
})
}
func TestObjectService_ListTemplates(t *testing.T) {
t.Run("templates found", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
// Mock template type search
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("template-type-id"),
bundle.RelationKeyUniqueKey.String(): pbtypes.String("ot-template"),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock actual template objects search
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("template-1"),
bundle.RelationKeyTargetObjectType.String(): pbtypes.String("target-type-id"),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock object show for template details
fx.mwMock.On("ObjectShow", mock.Anything, mock.Anything).Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String("Template Name"),
bundle.RelationKeyIconEmoji.String(): pbtypes.String("📝"),
},
},
},
},
},
}, nil).Once()
// when
templates, total, hasMore, err := fx.ListTemplates(ctx, mockedSpaceId, "target-type-id", offset, limit)
// then
require.NoError(t, err)
require.Len(t, templates, 1)
require.Equal(t, "template-1", templates[0].Id)
require.Equal(t, "Template Name", templates[0].Name)
require.Equal(t, "📝", templates[0].Icon)
require.Equal(t, 1, total)
require.False(t, hasMore)
})
t.Run("no template type found", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// when
templates, total, hasMore, err := fx.ListTemplates(ctx, mockedSpaceId, "missing-type-id", offset, limit)
// then
require.ErrorIs(t, err, ErrTemplateTypeNotFound)
require.Len(t, templates, 0)
require.Equal(t, 0, total)
require.False(t, hasMore)
})
}
func TestObjectService_GetTemplate(t *testing.T) {
t.Run("template found", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedTemplateId,
}).Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
ObjectView: &model.ObjectView{
Details: []*model.ObjectViewDetailsSet{
{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedTemplateId),
bundle.RelationKeyName.String(): pbtypes.String(mockedTemplateName),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedTemplateIcon),
},
},
},
},
},
}).Once()
// when
template, err := fx.GetTemplate(ctx, mockedSpaceId, mockedObjectType, mockedTemplateId)
// then
require.NoError(t, err)
require.Equal(t, mockedTemplateId, template.Id)
require.Equal(t, mockedTemplateName, template.Name)
require.Equal(t, mockedTemplateIcon, template.Icon)
})
t.Run("template not found", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedTemplateId,
}).Return(&pb.RpcObjectShowResponse{
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NOT_FOUND},
}).Once()
// when
template, err := fx.GetTemplate(ctx, mockedSpaceId, mockedTypeId, mockedTemplateId)
// then
require.ErrorIs(t, err, ErrTemplateNotFound)
require.Empty(t, template)
})
}

View file

@ -0,0 +1,92 @@
package search
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/core/api/util"
)
// GlobalSearchHandler searches and retrieves objects across all spaces
//
// @Summary Search objects across all spaces
// @Tags search
// @Accept json
// @Produce json
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Param request body SearchRequest true "Search parameters"
// @Success 200 {object} pagination.PaginatedResponse[object.Object] "List of objects"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /search [post]
func GlobalSearchHandler(s *SearchService) gin.HandlerFunc {
return func(c *gin.Context) {
offset := c.GetInt("offset")
limit := c.GetInt("limit")
request := SearchRequest{}
if err := c.BindJSON(&request); err != nil {
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
objects, total, hasMore, err := s.GlobalSearch(c, request, offset, limit)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedSearchObjects, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
pagination.RespondWithPagination(c, http.StatusOK, objects, total, offset, limit, hasMore)
}
}
// SearchHandler searches and retrieves objects within a space
//
// @Summary Search objects within a space
// @Tags search
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Param request body SearchRequest true "Search parameters"
// @Success 200 {object} pagination.PaginatedResponse[object.Object] "List of objects"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/search [post]
func SearchHandler(s *SearchService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceID := c.Param("space_id")
offset := c.GetInt("offset")
limit := c.GetInt("limit")
request := SearchRequest{}
if err := c.BindJSON(&request); err != nil {
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
objects, total, hasMore, err := s.Search(c, spaceID, request, offset, limit)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedSearchObjects, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
pagination.RespondWithPagination(c, http.StatusOK, objects, total, offset, limit, hasMore)
}
}

View file

@ -0,0 +1,12 @@
package search
type SearchRequest struct {
Query string `json:"query"`
Types []string `json:"types"`
Sort SortOptions `json:"sort"`
}
type SortOptions struct {
Direction string `json:"direction" enums:"asc,desc" default:"desc"`
Timestamp string `json:"timestamp" enums:"created_date,last_modified_date,last_opened_date" default:"last_modified_date"`
}

View file

@ -0,0 +1,299 @@
package search
import (
"context"
"errors"
"sort"
"strings"
"github.com/anyproto/anytype-heart/core/api/internal/object"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
var (
spaceLimit = 64
ErrFailedSearchObjects = errors.New("failed to retrieve objects from space")
)
type Service interface {
GlobalSearch(ctx context.Context, request SearchRequest, offset int, limit int) (objects []object.Object, total int, hasMore bool, err error)
Search(ctx context.Context, spaceId string, request SearchRequest, offset int, limit int) (objects []object.Object, total int, hasMore bool, err error)
}
type SearchService struct {
mw service.ClientCommandsServer
spaceService *space.SpaceService
objectService *object.ObjectService
AccountInfo *model.AccountInfo
}
func NewService(mw service.ClientCommandsServer, spaceService *space.SpaceService, objectService *object.ObjectService) *SearchService {
return &SearchService{mw: mw, spaceService: spaceService, objectService: objectService}
}
// GlobalSearch retrieves a paginated list of objects from all spaces that match the search parameters.
func (s *SearchService) GlobalSearch(ctx context.Context, request SearchRequest, offset int, limit int) (objects []object.Object, total int, hasMore bool, err error) {
spaces, _, _, err := s.spaceService.ListSpaces(ctx, 0, spaceLimit)
if err != nil {
return nil, 0, false, err
}
baseFilters := s.prepareBaseFilters()
queryFilters := s.prepareQueryFilter(request.Query)
sorts := s.prepareSorts(request.Sort)
dateToSortAfter := sorts.RelationKey
allResponses := make([]*pb.RpcObjectSearchResponse, 0, len(spaces))
for _, space := range spaces {
// Resolve object type IDs per space, as they are unique per space
typeFilters := s.prepareObjectTypeFilters(space.Id, request.Types)
filters := s.combineFilters(model.BlockContentDataviewFilter_And, baseFilters, queryFilters, typeFilters)
objResp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: space.Id,
Filters: filters,
Sorts: []*model.BlockContentDataviewSort{sorts},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeySpaceId.String(), dateToSortAfter},
Limit: int32(offset + limit), // nolint: gosec
})
if objResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return nil, 0, false, ErrFailedSearchObjects
}
allResponses = append(allResponses, objResp)
}
combinedRecords := make([]struct {
Id string
SpaceId string
DateToSortAfter float64
}, 0)
for _, objResp := range allResponses {
for _, record := range objResp.Records {
combinedRecords = append(combinedRecords, struct {
Id string
SpaceId string
DateToSortAfter float64
}{
Id: record.Fields[bundle.RelationKeyId.String()].GetStringValue(),
SpaceId: record.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(),
DateToSortAfter: record.Fields[dateToSortAfter].GetNumberValue(),
})
}
}
// sort after posix last_modified_date to achieve descending sort order across all spaces
sort.Slice(combinedRecords, func(i, j int) bool {
return combinedRecords[i].DateToSortAfter > combinedRecords[j].DateToSortAfter
})
total = len(combinedRecords)
paginatedRecords, hasMore := pagination.Paginate(combinedRecords, offset, limit)
results := make([]object.Object, 0, len(paginatedRecords))
for _, record := range paginatedRecords {
object, err := s.objectService.GetObject(ctx, record.SpaceId, record.Id)
if err != nil {
return nil, 0, false, err
}
results = append(results, object)
}
return results, total, hasMore, nil
}
// Search retrieves a paginated list of objects from a specific space that match the search parameters.
func (s *SearchService) Search(ctx context.Context, spaceId string, request SearchRequest, offset int, limit int) (objects []object.Object, total int, hasMore bool, err error) {
baseFilters := s.prepareBaseFilters()
queryFilters := s.prepareQueryFilter(request.Query)
typeFilters := s.prepareObjectTypeFilters(spaceId, request.Types)
filters := s.combineFilters(model.BlockContentDataviewFilter_And, baseFilters, queryFilters, typeFilters)
sorts := s.prepareSorts(request.Sort)
dateToSortAfter := sorts.RelationKey
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: filters,
Sorts: []*model.BlockContentDataviewSort{sorts},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeySpaceId.String(), dateToSortAfter},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return nil, 0, false, ErrFailedSearchObjects
}
total = len(resp.Records)
paginatedRecords, hasMore := pagination.Paginate(resp.Records, offset, limit)
results := make([]object.Object, 0, len(paginatedRecords))
for _, record := range paginatedRecords {
object, err := s.objectService.GetObject(ctx, record.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(), record.Fields[bundle.RelationKeyId.String()].GetStringValue())
if err != nil {
return nil, 0, false, err
}
results = append(results, object)
}
return results, total, hasMore, nil
}
// makeAndCondition combines multiple filter groups with the given operator.
func (s *SearchService) combineFilters(operator model.BlockContentDataviewFilterOperator, filterGroups ...[]*model.BlockContentDataviewFilter) []*model.BlockContentDataviewFilter {
nestedFilters := make([]*model.BlockContentDataviewFilter, 0)
for _, group := range filterGroups {
if len(group) > 0 {
nestedFilters = append(nestedFilters, group...)
}
}
if len(nestedFilters) == 0 {
return nil
}
return []*model.BlockContentDataviewFilter{
{
Operator: operator,
NestedFilters: nestedFilters,
},
}
}
// prepareBaseFilters returns a list of default filters that should be applied to all search queries.
func (s *SearchService) prepareBaseFilters() []*model.BlockContentDataviewFilter {
return []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.IntList([]int{
int(model.ObjectType_basic),
int(model.ObjectType_profile),
int(model.ObjectType_todo),
int(model.ObjectType_note),
int(model.ObjectType_bookmark),
int(model.ObjectType_set),
int(model.ObjectType_collection),
int(model.ObjectType_participant),
}...),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyIsHidden.String(),
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.Bool(true),
},
}
}
// prepareQueryFilter combines object name and snippet filters with an OR condition.
func (s *SearchService) prepareQueryFilter(searchQuery string) []*model.BlockContentDataviewFilter {
if searchQuery == "" {
return nil
}
return []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_Or,
NestedFilters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyName.String(),
Condition: model.BlockContentDataviewFilter_Like,
Value: pbtypes.String(searchQuery),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeySnippet.String(),
Condition: model.BlockContentDataviewFilter_Like,
Value: pbtypes.String(searchQuery),
},
},
},
}
}
// prepareObjectTypeFilters combines object type filters with an OR condition.
func (s *SearchService) prepareObjectTypeFilters(spaceId string, objectTypes []string) []*model.BlockContentDataviewFilter {
if len(objectTypes) == 0 || objectTypes[0] == "" {
return nil
}
// Prepare nested filters for each object type
nestedFilters := make([]*model.BlockContentDataviewFilter, 0, len(objectTypes))
for _, objectType := range objectTypes {
typeId := objectType
if strings.HasPrefix(objectType, "ot-") {
var err error
typeId, err = util.ResolveUniqueKeyToTypeId(s.mw, spaceId, objectType)
if err != nil {
continue
}
}
nestedFilters = append(nestedFilters, &model.BlockContentDataviewFilter{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyType.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(typeId),
})
}
if len(nestedFilters) == 0 {
return nil
}
// Combine all filters with an OR operator
return []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_Or,
NestedFilters: nestedFilters,
},
}
}
// prepareSorts returns a sort filter based on the given sort parameters
func (s *SearchService) prepareSorts(sort SortOptions) *model.BlockContentDataviewSort {
return &model.BlockContentDataviewSort{
RelationKey: s.getSortRelationKey(sort.Timestamp),
Type: s.getSortDirection(sort.Direction),
Format: model.RelationFormat_date,
IncludeTime: true,
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
}
}
// getSortRelationKey returns the relation key for the given sort timestamp
func (s *SearchService) getSortRelationKey(timestamp string) string {
switch timestamp {
case "created_date":
return bundle.RelationKeyCreatedDate.String()
case "last_modified_date":
return bundle.RelationKeyLastModifiedDate.String()
case "last_opened_date":
return bundle.RelationKeyLastOpenedDate.String()
default:
return bundle.RelationKeyLastModifiedDate.String()
}
}
// getSortDirection returns the sort direction for the given string
func (s *SearchService) getSortDirection(direction string) model.BlockContentDataviewSortType {
switch direction {
case "asc":
return model.BlockContentDataviewSort_Asc
case "desc":
return model.BlockContentDataviewSort_Desc
default:
return model.BlockContentDataviewSort_Desc
}
}

View file

@ -0,0 +1,596 @@
package search
import (
"context"
"testing"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/api/internal/object"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service/mock_service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
const (
offset = 0
limit = 100
techSpaceId = "tech-space-id"
gatewayUrl = "http://localhost:31006"
mockedSpaceId = "mocked-space-id"
mockedSearchTerm = "mocked-search-term"
mockedObjectId = "mocked-object-id"
mockedObjectName = "mocked-object-name"
mockedRootId = "mocked-root-id"
mockedParticipantId = "mocked-participant-id"
mockedType = "mocked-type"
mockedTagId1 = "mocked-tag-id-1"
mockedTagValue1 = "mocked-tag-value-1"
mockedTagColor1 = "mocked-tag-color-1"
mockedTagId2 = "mocked-tag-id-2"
mockedTagValue2 = "mocked-tag-value-2"
mockedTagColor2 = "mocked-tag-color-2"
mockedObjectTypeName = "mocked-object-type-name"
mockedParticipantName = "mocked-participant-name"
mockedParticipantIcon = "mocked-participant-icon"
mockedParticipantImage = "mocked-participant-image"
mockedParticipantIdentity = "mocked-participant-identity"
mockedParticipantGlobalName = "mocked-participant-global-name"
)
type fixture struct {
*SearchService
mwMock *mock_service.MockClientCommandsServer
}
func newFixture(t *testing.T) *fixture {
mw := mock_service.NewMockClientCommandsServer(t)
spaceService := space.NewService(mw)
spaceService.AccountInfo = &model.AccountInfo{TechSpaceId: techSpaceId, GatewayUrl: gatewayUrl}
objectService := object.NewService(mw, spaceService)
objectService.AccountInfo = &model.AccountInfo{TechSpaceId: techSpaceId}
searchService := NewService(mw, spaceService, objectService)
searchService.AccountInfo = &model.AccountInfo{
TechSpaceId: techSpaceId,
GatewayUrl: gatewayUrl,
}
return &fixture{
SearchService: searchService,
mwMock: mw,
}
}
func TestSearchService_GlobalSearch(t *testing.T) {
t.Run("objects found globally", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
// Mock retrieving spaces first
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: techSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ObjectType_spaceView)),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeySpaceLocalStatus.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)),
},
},
Sorts: []*model.BlockContentDataviewSort{
{
RelationKey: bundle.RelationKeySpaceOrder.String(),
Type: model.BlockContentDataviewSort_Asc,
NoCollate: true,
EmptyPlacement: model.BlockContentDataviewSort_End,
},
},
Keys: []string{bundle.RelationKeyTargetSpaceId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String()},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyTargetSpaceId.String(): pbtypes.String(mockedSpaceId),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock workspace opening
fx.mwMock.On("WorkspaceOpen", mock.Anything, &pb.RpcWorkspaceOpenRequest{
SpaceId: mockedSpaceId,
WithChat: true,
}).Return(&pb.RpcWorkspaceOpenResponse{
Info: &model.AccountInfo{
TechSpaceId: mockedSpaceId,
},
Error: &pb.RpcWorkspaceOpenResponseError{Code: pb.RpcWorkspaceOpenResponseError_NULL},
}).Once()
// Mock objects in space
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_And,
NestedFilters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.IntList([]int{
int(model.ObjectType_basic),
int(model.ObjectType_profile),
int(model.ObjectType_todo),
int(model.ObjectType_note),
int(model.ObjectType_bookmark),
int(model.ObjectType_set),
int(model.ObjectType_collection),
int(model.ObjectType_participant),
}...),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyIsHidden.String(),
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.Bool(true),
},
{
Operator: model.BlockContentDataviewFilter_Or,
NestedFilters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyName.String(),
Condition: model.BlockContentDataviewFilter_Like,
Value: pbtypes.String(mockedSearchTerm),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeySnippet.String(),
Condition: model.BlockContentDataviewFilter_Like,
Value: pbtypes.String(mockedSearchTerm),
},
},
},
},
},
},
Sorts: []*model.BlockContentDataviewSort{{
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
Type: model.BlockContentDataviewSort_Desc,
Format: model.RelationFormat_date,
IncludeTime: true,
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
}},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeySpaceId.String(), bundle.RelationKeyLastModifiedDate.String()},
Limit: int32(offset + limit),
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock object show for object blocks and details
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedObjectId,
}).Return(&pb.RpcObjectShowResponse{
ObjectView: &model.ObjectView{
RootId: mockedRootId,
Blocks: []*model.Block{
{
Id: mockedRootId,
Restrictions: &model.BlockRestrictions{
Read: false,
Edit: false,
Remove: false,
Drag: false,
DropOn: false,
},
ChildrenIds: []string{"header", "text-block"},
},
{
Id: "header",
Restrictions: &model.BlockRestrictions{
Read: false,
Edit: true,
Remove: true,
Drag: true,
DropOn: true,
},
ChildrenIds: []string{"title", "featuredRelations"},
},
{
Id: "text-block",
Content: &model.BlockContentOfText{
Text: &model.BlockContentText{
Text: "This is a sample text block",
Style: model.BlockContentText_Paragraph,
},
},
},
},
Details: []*model.ObjectViewDetailsSet{
{
Id: mockedRootId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeyLayout.String(): pbtypes.Int64(int64(model.ObjectType_basic)),
bundle.RelationKeyIconEmoji.String(): pbtypes.String("🌐"),
bundle.RelationKeyLastModifiedDate.String(): pbtypes.Float64(999999),
bundle.RelationKeyLastModifiedBy.String(): pbtypes.String(mockedParticipantId),
bundle.RelationKeyCreatedDate.String(): pbtypes.Float64(888888),
bundle.RelationKeyCreator.String(): pbtypes.String(mockedParticipantId),
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
bundle.RelationKeyType.String(): pbtypes.String(mockedType),
bundle.RelationKeyTag.String(): pbtypes.StringList([]string{mockedTagId1, mockedTagId2}),
},
},
},
{
Id: mockedParticipantId,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedParticipantId),
},
},
},
{
Id: mockedTagId1,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedTagValue1),
bundle.RelationKeyRelationOptionColor.String(): pbtypes.String(mockedTagColor1),
},
},
},
{
Id: mockedTagId2,
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedTagValue2),
bundle.RelationKeyRelationOptionColor.String(): pbtypes.String(mockedTagColor2),
},
},
},
},
},
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
}, nil).Once()
// Mock type resolution
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(mockedType),
},
},
Keys: []string{bundle.RelationKeyName.String()},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectTypeName),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock participant details
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(mockedParticipantId),
},
},
Keys: []string{bundle.RelationKeyId.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyIconEmoji.String(),
bundle.RelationKeyIconImage.String(),
bundle.RelationKeyIdentity.String(),
bundle.RelationKeyGlobalName.String(),
bundle.RelationKeyParticipantPermissions.String(),
},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedParticipantId),
bundle.RelationKeyName.String(): pbtypes.String(mockedParticipantName),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedParticipantIcon),
bundle.RelationKeyIconImage.String(): pbtypes.String(mockedParticipantImage),
bundle.RelationKeyIdentity.String(): pbtypes.String(mockedParticipantIdentity),
bundle.RelationKeyGlobalName.String(): pbtypes.String(mockedParticipantGlobalName),
bundle.RelationKeyParticipantPermissions.String(): pbtypes.Int64(int64(model.ParticipantPermissions_Reader)),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Twice()
// when
objects, total, hasMore, err := fx.GlobalSearch(ctx, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Direction: "desc", Timestamp: "last_modified_date"}}, offset, limit)
// then
require.NoError(t, err)
require.Len(t, objects, 1)
require.Equal(t, mockedObjectTypeName, objects[0].Type)
require.Equal(t, mockedSpaceId, objects[0].SpaceId)
require.Equal(t, mockedObjectName, objects[0].Name)
require.Equal(t, mockedObjectId, objects[0].Id)
require.Equal(t, model.ObjectTypeLayout_name[int32(model.ObjectType_basic)], objects[0].Layout)
require.Equal(t, "🌐", objects[0].Icon)
require.Equal(t, "This is a sample text block", objects[0].Blocks[2].Text.Text)
// check details
for _, detail := range objects[0].Details {
if detail.Id == "created_date" {
require.Equal(t, "1970-01-11T06:54:48Z", detail.Details["created_date"])
} else if detail.Id == "last_modified_date" {
require.Equal(t, "1970-01-12T13:46:39Z", detail.Details["last_modified_date"])
} else if detail.Id == "created_by" {
require.Equal(t, mockedParticipantId, detail.Details["details"].(space.Member).Id)
require.Equal(t, mockedParticipantName, detail.Details["details"].(space.Member).Name)
require.Equal(t, gatewayUrl+"/image/"+mockedParticipantImage, detail.Details["details"].(space.Member).Icon)
require.Equal(t, mockedParticipantIdentity, detail.Details["details"].(space.Member).Identity)
require.Equal(t, mockedParticipantGlobalName, detail.Details["details"].(space.Member).GlobalName)
} else if detail.Id == "last_modified_by" {
require.Equal(t, mockedParticipantId, detail.Details["details"].(space.Member).Id)
}
}
// check tags
tags := []object.Tag{}
for _, detail := range objects[0].Details {
if tagList, ok := detail.Details["tags"].([]object.Tag); ok {
for _, tag := range tagList {
tags = append(tags, tag)
}
}
}
require.Len(t, tags, 2)
require.Equal(t, mockedTagId1, tags[0].Id)
require.Equal(t, mockedTagValue1, tags[0].Name)
require.Equal(t, mockedTagColor1, tags[0].Color)
require.Equal(t, mockedTagId2, tags[1].Id)
require.Equal(t, mockedTagValue2, tags[1].Name)
require.Equal(t, mockedTagColor2, tags[1].Color)
require.Equal(t, 1, total)
require.False(t, hasMore)
})
}
func TestSearchService_Search(t *testing.T) {
t.Run("objects found in a specific space", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
// Mock objects in space
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_And,
NestedFilters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.IntList([]int{
int(model.ObjectType_basic),
int(model.ObjectType_profile),
int(model.ObjectType_todo),
int(model.ObjectType_note),
int(model.ObjectType_bookmark),
int(model.ObjectType_set),
int(model.ObjectType_collection),
int(model.ObjectType_participant),
}...),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyIsHidden.String(),
Condition: model.BlockContentDataviewFilter_NotEqual,
Value: pbtypes.Bool(true),
},
{
Operator: model.BlockContentDataviewFilter_Or,
NestedFilters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyName.String(),
Condition: model.BlockContentDataviewFilter_Like,
Value: pbtypes.String(mockedSearchTerm),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeySnippet.String(),
Condition: model.BlockContentDataviewFilter_Like,
Value: pbtypes.String(mockedSearchTerm),
},
},
},
},
},
},
Sorts: []*model.BlockContentDataviewSort{{
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
Type: model.BlockContentDataviewSort_Desc,
Format: model.RelationFormat_date,
IncludeTime: true,
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
}},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeySpaceId.String(), bundle.RelationKeyLastModifiedDate.String()},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock object show for object details
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
SpaceId: mockedSpaceId,
ObjectId: mockedObjectId,
}).Return(&pb.RpcObjectShowResponse{
ObjectView: &model.ObjectView{
RootId: mockedRootId,
Details: []*model.ObjectViewDetailsSet{
{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
bundle.RelationKeyLayout.String(): pbtypes.Int64(int64(model.ObjectType_basic)),
bundle.RelationKeyLastModifiedDate.String(): pbtypes.Float64(999999),
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
bundle.RelationKeyType.String(): pbtypes.String(mockedType),
},
},
},
},
},
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
}).Once()
// Mock type resolution
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(mockedType),
},
},
Keys: []string{bundle.RelationKeyName.String()},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectTypeName),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// Mock participant details
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: mockedSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(""),
},
},
Keys: []string{bundle.RelationKeyId.String(),
bundle.RelationKeyName.String(),
bundle.RelationKeyIconEmoji.String(),
bundle.RelationKeyIconImage.String(),
bundle.RelationKeyIdentity.String(),
bundle.RelationKeyGlobalName.String(),
bundle.RelationKeyParticipantPermissions.String(),
},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Twice()
// when
objects, total, hasMore, err := fx.Search(ctx, mockedSpaceId, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Direction: "desc", Timestamp: "last_modified_date"}}, offset, limit)
// then
require.NoError(t, err)
require.Len(t, objects, 1)
require.Equal(t, mockedObjectName, objects[0].Name)
require.Equal(t, mockedObjectId, objects[0].Id)
require.Equal(t, mockedSpaceId, objects[0].SpaceId)
require.Equal(t, model.ObjectTypeLayout_name[int32(model.ObjectType_basic)], objects[0].Layout)
require.Equal(t, 1, total)
require.False(t, hasMore)
})
t.Run("no objects found in space", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// when
objects, total, hasMore, err := fx.Search(ctx, mockedSpaceId, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Direction: "desc", Timestamp: "last_modified_date"}}, offset, limit)
// then
require.NoError(t, err)
require.Len(t, objects, 0)
require.Equal(t, 0, total)
require.False(t, hasMore)
})
t.Run("error during search", func(t *testing.T) {
// given
ctx := context.Background()
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_UNKNOWN_ERROR},
}).Once()
// when
objects, total, hasMore, err := fx.Search(ctx, mockedSpaceId, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Direction: "desc", Timestamp: "last_modified_date"}}, offset, limit)
// then
require.Error(t, err)
require.Empty(t, objects)
require.Equal(t, 0, total)
require.False(t, hasMore)
})
}

View file

@ -0,0 +1,113 @@
package space
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/core/api/util"
)
// GetSpacesHandler retrieves a list of spaces
//
// @Summary List spaces
// @Tags spaces
// @Accept json
// @Produce json
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Success 200 {object} pagination.PaginatedResponse[Space] "List of spaces"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces [get]
func GetSpacesHandler(s *SpaceService) gin.HandlerFunc {
return func(c *gin.Context) {
offset := c.GetInt("offset")
limit := c.GetInt("limit")
spaces, total, hasMore, err := s.ListSpaces(c.Request.Context(), offset, limit)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedListSpaces, http.StatusInternalServerError),
util.ErrToCode(ErrFailedOpenWorkspace, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
pagination.RespondWithPagination(c, http.StatusOK, spaces, total, offset, limit, hasMore)
}
}
// CreateSpaceHandler creates a new space
//
// @Summary Create space
// @Tags spaces
// @Accept json
// @Produce json
// @Param name body CreateSpaceRequest true "Space to create"
// @Success 200 {object} CreateSpaceResponse "Space created successfully"
// @Failure 400 {object} util.ValidationError "Bad request"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces [post]
func CreateSpaceHandler(s *SpaceService) gin.HandlerFunc {
return func(c *gin.Context) {
nameRequest := CreateSpaceRequest{}
if err := c.BindJSON(&nameRequest); err != nil {
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
c.JSON(http.StatusBadRequest, apiErr)
return
}
space, err := s.CreateSpace(c.Request.Context(), nameRequest.Name)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedCreateSpace, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
c.JSON(http.StatusOK, CreateSpaceResponse{Space: space})
}
}
// GetMembersHandler retrieves a list of members in a space
//
// @Summary List members
// @Tags spaces
// @Accept json
// @Produce json
// @Param space_id path string true "Space ID"
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
// @Success 200 {object} pagination.PaginatedResponse[Member] "List of members"
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
// @Failure 500 {object} util.ServerError "Internal server error"
// @Router /spaces/{space_id}/members [get]
func GetMembersHandler(s *SpaceService) gin.HandlerFunc {
return func(c *gin.Context) {
spaceId := c.Param("space_id")
offset := c.GetInt("offset")
limit := c.GetInt("limit")
members, total, hasMore, err := s.ListMembers(c.Request.Context(), spaceId, offset, limit)
code := util.MapErrorCode(err,
util.ErrToCode(ErrFailedListMembers, http.StatusInternalServerError),
)
if code != http.StatusOK {
apiErr := util.CodeToAPIError(code, err.Error())
c.JSON(code, apiErr)
return
}
pagination.RespondWithPagination(c, http.StatusOK, members, total, offset, limit, hasMore)
}
}

View file

@ -0,0 +1,41 @@
package space
type CreateSpaceRequest struct {
Name string `json:"name" example:"New Space"`
}
type CreateSpaceResponse struct {
Space Space `json:"space"`
}
type Space struct {
Type string `json:"type" example:"space"`
Id string `json:"id" example:"bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1"`
Name string `json:"name" example:"Space Name"`
Icon string `json:"icon" example:"http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay"`
HomeObjectId string `json:"home_object_id" example:"bafyreie4qcl3wczb4cw5hrfyycikhjyh6oljdis3ewqrk5boaav3sbwqya"`
ArchiveObjectId string `json:"archive_object_id" example:"bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri"`
ProfileObjectId string `json:"profile_object_id" example:"bafyreiaxhwreshjqwndpwtdsu4mtihaqhhmlygqnyqpfyfwlqfq3rm3gw4"`
MarketplaceWorkspaceId string `json:"marketplace_workspace_id" example:"_anytype_marketplace"`
WorkspaceObjectId string `json:"workspace_object_id" example:"bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y"`
DeviceId string `json:"device_id" example:"12D3KooWGZMJ4kQVyQVXaj7gJPZr3RZ2nvd9M2Eq2pprEoPih9WF"`
AccountSpaceId string `json:"account_space_id" example:"bafyreihpd2knon5wbljhtfeg3fcqtg3i2pomhhnigui6lrjmzcjzep7gcy.23me69r569oi1"`
WidgetsId string `json:"widgets_id" example:"bafyreialj7pceh53mifm5dixlho47ke4qjmsn2uh4wsjf7xq2pnlo5xfva"`
SpaceViewId string `json:"space_view_id" example:"bafyreigzv3vq7qwlrsin6njoduq727ssnhwd6bgyfj6nm4hv3pxoc2rxhy"`
TechSpaceId string `json:"tech_space_id" example:"bafyreif4xuwncrjl6jajt4zrrfnylpki476nv2w64yf42ovt7gia7oypii.23me69r569oi1"`
GatewayUrl string `json:"gateway_url" example:"http://127.0.0.1:31006"`
LocalStoragePath string `json:"local_storage_path" example:"/Users/johndoe/Library/Application Support/Anytype/data/AAHTtt1wuQEnaYBNZ2Cyfcvs6DqPqxgn8VXDVk4avsUkMuha"`
Timezone string `json:"timezone" example:""`
AnalyticsId string `json:"analytics_id" example:"624aecdd-4797-4611-9d61-a2ae5f53cf1c"`
NetworkId string `json:"network_id" example:"N83gJpVd9MuNRZAuJLZ7LiMntTThhPc6DtzWWVjb1M3PouVU"`
}
type Member struct {
Type string `json:"type" example:"member"`
Id string `json:"id" example:"_participant_bafyreigyfkt6rbv24sbv5aq2hko1bhmv5xxlf22b4bypdu6j7hnphm3psq_23me69r569oi1_AAjEaEwPF4nkEh9AWkqEnzcQ8HziBB4ETjiTpvRCQvWnSMDZ"`
Name string `json:"name" example:"John Doe"`
Icon string `json:"icon" example:"http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100"`
Identity string `json:"identity" example:"AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ"`
GlobalName string `json:"global_name" example:"john.any"`
Role string `json:"role" enums:"Reader,Writer,Owner,NoPermission" example:"Owner"`
}

View file

@ -0,0 +1,242 @@
package space
import (
"context"
"crypto/rand"
"errors"
"math/big"
"github.com/gogo/protobuf/types"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/core/api/util"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
var (
ErrFailedListSpaces = errors.New("failed to retrieve list of spaces")
ErrFailedOpenWorkspace = errors.New("failed to open workspace")
ErrFailedGenerateRandomIcon = errors.New("failed to generate random icon")
ErrFailedCreateSpace = errors.New("failed to create space")
ErrFailedListMembers = errors.New("failed to retrieve list of members")
)
type Service interface {
ListSpaces(ctx context.Context, offset int, limit int) ([]Space, int, bool, error)
CreateSpace(ctx context.Context, name string) (Space, error)
ListMembers(ctx context.Context, spaceId string, offset int, limit int) ([]Member, int, bool, error)
}
type SpaceService struct {
mw service.ClientCommandsServer
AccountInfo *model.AccountInfo
}
func NewService(mw service.ClientCommandsServer) *SpaceService {
return &SpaceService{mw: mw}
}
// ListSpaces returns a paginated list of spaces for the account.
func (s *SpaceService) ListSpaces(ctx context.Context, offset int, limit int) (spaces []Space, total int, hasMore bool, err error) {
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: s.AccountInfo.TechSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ObjectType_spaceView)),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeySpaceLocalStatus.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)),
},
},
Sorts: []*model.BlockContentDataviewSort{
{
RelationKey: bundle.RelationKeySpaceOrder.String(),
Type: model.BlockContentDataviewSort_Asc,
NoCollate: true,
EmptyPlacement: model.BlockContentDataviewSort_End,
},
},
Keys: []string{bundle.RelationKeyTargetSpaceId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return nil, 0, false, ErrFailedListSpaces
}
total = len(resp.Records)
paginatedRecords, hasMore := pagination.Paginate(resp.Records, offset, limit)
spaces = make([]Space, 0, len(paginatedRecords))
for _, record := range paginatedRecords {
workspace, err := s.getWorkspaceInfo(record.Fields[bundle.RelationKeyTargetSpaceId.String()].GetStringValue())
if err != nil {
return nil, 0, false, err
}
// TODO: name and icon are only returned here; fix that
workspace.Name = record.Fields[bundle.RelationKeyName.String()].GetStringValue()
workspace.Icon = util.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), record.Fields[bundle.RelationKeyIconImage.String()].GetStringValue())
spaces = append(spaces, workspace)
}
return spaces, total, hasMore, nil
}
// CreateSpace creates a new space with the given name and returns the space info.
func (s *SpaceService) CreateSpace(ctx context.Context, name string) (Space, error) {
iconOption, err := rand.Int(rand.Reader, big.NewInt(13))
if err != nil {
return Space{}, ErrFailedGenerateRandomIcon
}
// Create new workspace with a random icon and import default use case
resp := s.mw.WorkspaceCreate(ctx, &pb.RpcWorkspaceCreateRequest{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyIconOption.String(): pbtypes.Float64(float64(iconOption.Int64())),
bundle.RelationKeyName.String(): pbtypes.String(name),
bundle.RelationKeySpaceDashboardId.String(): pbtypes.String("lastOpened"),
},
},
UseCase: pb.RpcObjectImportUseCaseRequest_GET_STARTED,
WithChat: true,
})
if resp.Error.Code != pb.RpcWorkspaceCreateResponseError_NULL {
return Space{}, ErrFailedCreateSpace
}
return s.getWorkspaceInfo(resp.SpaceId)
}
// ListMembers returns a paginated list of members in the space with the given ID.
func (s *SpaceService) ListMembers(ctx context.Context, spaceId string, offset int, limit int) (members []Member, total int, hasMore bool, err error) {
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ObjectType_participant)),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyParticipantStatus.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ParticipantStatus_Active)),
},
},
Sorts: []*model.BlockContentDataviewSort{
{
RelationKey: bundle.RelationKeyName.String(),
Type: model.BlockContentDataviewSort_Asc,
},
},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String(), bundle.RelationKeyIdentity.String(), bundle.RelationKeyGlobalName.String(), bundle.RelationKeyParticipantPermissions.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return nil, 0, false, ErrFailedListMembers
}
total = len(resp.Records)
paginatedMembers, hasMore := pagination.Paginate(resp.Records, offset, limit)
members = make([]Member, 0, len(paginatedMembers))
for _, record := range paginatedMembers {
icon := util.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), record.Fields[bundle.RelationKeyIconImage.String()].GetStringValue())
member := Member{
Type: "member",
Id: record.Fields[bundle.RelationKeyId.String()].GetStringValue(),
Name: record.Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: icon,
Identity: record.Fields[bundle.RelationKeyIdentity.String()].GetStringValue(),
GlobalName: record.Fields[bundle.RelationKeyGlobalName.String()].GetStringValue(),
Role: model.ParticipantPermissions_name[int32(record.Fields[bundle.RelationKeyParticipantPermissions.String()].GetNumberValue())],
}
members = append(members, member)
}
return members, total, hasMore, nil
}
func (s *SpaceService) GetParticipantDetails(mw service.ClientCommandsServer, spaceId string, participantId string) Member {
resp := mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(participantId),
},
},
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String(), bundle.RelationKeyIdentity.String(), bundle.RelationKeyGlobalName.String(), bundle.RelationKeyParticipantPermissions.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return Member{}
}
if len(resp.Records) == 0 {
return Member{}
}
icon := util.GetIconFromEmojiOrImage(s.AccountInfo, "", resp.Records[0].Fields[bundle.RelationKeyIconImage.String()].GetStringValue())
return Member{
Type: "member",
Id: resp.Records[0].Fields[bundle.RelationKeyId.String()].GetStringValue(),
Name: resp.Records[0].Fields[bundle.RelationKeyName.String()].GetStringValue(),
Icon: icon,
Identity: resp.Records[0].Fields[bundle.RelationKeyIdentity.String()].GetStringValue(),
GlobalName: resp.Records[0].Fields[bundle.RelationKeyGlobalName.String()].GetStringValue(),
Role: model.ParticipantPermissions_name[int32(resp.Records[0].Fields[bundle.RelationKeyParticipantPermissions.String()].GetNumberValue())],
}
}
// getWorkspaceInfo returns the workspace info for the space with the given ID.
func (s *SpaceService) getWorkspaceInfo(spaceId string) (space Space, err error) {
workspaceResponse := s.mw.WorkspaceOpen(context.Background(), &pb.RpcWorkspaceOpenRequest{
SpaceId: spaceId,
WithChat: true,
})
if workspaceResponse.Error.Code != pb.RpcWorkspaceOpenResponseError_NULL {
return Space{}, ErrFailedOpenWorkspace
}
return Space{
Type: "space",
Id: spaceId,
HomeObjectId: workspaceResponse.Info.HomeObjectId,
ArchiveObjectId: workspaceResponse.Info.ArchiveObjectId,
ProfileObjectId: workspaceResponse.Info.ProfileObjectId,
MarketplaceWorkspaceId: workspaceResponse.Info.MarketplaceWorkspaceId,
WorkspaceObjectId: workspaceResponse.Info.WorkspaceObjectId,
DeviceId: workspaceResponse.Info.DeviceId,
AccountSpaceId: workspaceResponse.Info.AccountSpaceId,
WidgetsId: workspaceResponse.Info.WidgetsId,
SpaceViewId: workspaceResponse.Info.SpaceViewId,
TechSpaceId: workspaceResponse.Info.TechSpaceId,
GatewayUrl: workspaceResponse.Info.GatewayUrl,
LocalStoragePath: workspaceResponse.Info.LocalStoragePath,
Timezone: workspaceResponse.Info.TimeZone,
AnalyticsId: workspaceResponse.Info.AnalyticsId,
NetworkId: workspaceResponse.Info.NetworkId,
}, nil
}

View file

@ -0,0 +1,313 @@
package space
import (
"regexp"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/gogo/protobuf/types"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service/mock_service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
const (
offset = 0
limit = 100
techSpaceId = "tech-space-id"
gatewayUrl = "http://localhost:31006"
iconImage = "bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri"
)
type fixture struct {
*SpaceService
mwMock *mock_service.MockClientCommandsServer
}
func newFixture(t *testing.T) *fixture {
mw := mock_service.NewMockClientCommandsServer(t)
spaceService := NewService(mw)
spaceService.AccountInfo = &model.AccountInfo{
TechSpaceId: techSpaceId,
GatewayUrl: gatewayUrl,
}
return &fixture{
SpaceService: spaceService,
mwMock: mw,
}
}
func TestSpaceService_ListSpaces(t *testing.T) {
t.Run("successful retrieval of spaces", func(t *testing.T) {
// given
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
SpaceId: techSpaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeyLayout.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.ObjectType_spaceView)),
},
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: bundle.RelationKeySpaceLocalStatus.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)),
},
},
Sorts: []*model.BlockContentDataviewSort{
{
RelationKey: "spaceOrder",
Type: model.BlockContentDataviewSort_Asc,
NoCollate: true,
EmptyPlacement: model.BlockContentDataviewSort_End,
},
},
Keys: []string{"targetSpaceId", "name", "iconEmoji", "iconImage"},
}).Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String("Another Workspace"),
bundle.RelationKeyTargetSpaceId.String(): pbtypes.String("another-space-id"),
bundle.RelationKeyIconEmoji.String(): pbtypes.String(""),
bundle.RelationKeyIconImage.String(): pbtypes.String(iconImage),
},
},
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String("My Workspace"),
bundle.RelationKeyTargetSpaceId.String(): pbtypes.String("my-space-id"),
bundle.RelationKeyIconEmoji.String(): pbtypes.String("🚀"),
bundle.RelationKeyIconImage.String(): pbtypes.String(""),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
fx.mwMock.On("WorkspaceOpen", mock.Anything, mock.Anything).Return(&pb.RpcWorkspaceOpenResponse{
Error: &pb.RpcWorkspaceOpenResponseError{Code: pb.RpcWorkspaceOpenResponseError_NULL},
Info: &model.AccountInfo{
HomeObjectId: "home-object-id",
ArchiveObjectId: "archive-object-id",
ProfileObjectId: "profile-object-id",
MarketplaceWorkspaceId: "marketplace-workspace-id",
WorkspaceObjectId: "workspace-object-id",
DeviceId: "device-id",
AccountSpaceId: "account-space-id",
WidgetsId: "widgets-id",
SpaceViewId: "space-view-id",
TechSpaceId: "tech-space-id",
GatewayUrl: "gateway-url",
LocalStoragePath: "local-storage-path",
TimeZone: "time-zone",
AnalyticsId: "analytics-id",
NetworkId: "network-id",
},
}, nil).Twice()
// when
spaces, total, hasMore, err := fx.ListSpaces(nil, offset, limit)
// then
require.NoError(t, err)
require.Len(t, spaces, 2)
require.Equal(t, "Another Workspace", spaces[0].Name)
require.Equal(t, "another-space-id", spaces[0].Id)
require.Regexpf(t, regexp.MustCompile(gatewayUrl+`/image/`+iconImage), spaces[0].Icon, "Icon URL does not match")
require.Equal(t, "My Workspace", spaces[1].Name)
require.Equal(t, "my-space-id", spaces[1].Id)
require.Equal(t, "🚀", spaces[1].Icon)
require.Equal(t, 2, total)
require.False(t, hasMore)
})
t.Run("no spaces found", func(t *testing.T) {
// given
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// when
spaces, total, hasMore, err := fx.ListSpaces(nil, offset, limit)
// then
require.NoError(t, err)
require.Len(t, spaces, 0)
require.Equal(t, 0, total)
require.False(t, hasMore)
})
t.Run("failed workspace open", func(t *testing.T) {
// given
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String("My Workspace"),
bundle.RelationKeyTargetSpaceId.String(): pbtypes.String("my-space-id"),
bundle.RelationKeyIconEmoji.String(): pbtypes.String("🚀"),
bundle.RelationKeyIconImage.String(): pbtypes.String(""),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
fx.mwMock.On("WorkspaceOpen", mock.Anything, mock.Anything).
Return(&pb.RpcWorkspaceOpenResponse{
Error: &pb.RpcWorkspaceOpenResponseError{Code: pb.RpcWorkspaceOpenResponseError_UNKNOWN_ERROR},
}, nil).Once()
// when
spaces, total, hasMore, err := fx.ListSpaces(nil, offset, limit)
// then
require.ErrorIs(t, err, ErrFailedOpenWorkspace)
require.Len(t, spaces, 0)
require.Equal(t, 0, total)
require.False(t, hasMore)
})
}
func TestSpaceService_CreateSpace(t *testing.T) {
t.Run("successful create space", func(t *testing.T) {
// given
fx := newFixture(t)
fx.mwMock.On("WorkspaceCreate", mock.Anything, mock.Anything).
Return(&pb.RpcWorkspaceCreateResponse{
Error: &pb.RpcWorkspaceCreateResponseError{Code: pb.RpcWorkspaceCreateResponseError_NULL},
SpaceId: "new-space-id",
}).Once()
fx.mwMock.On("WorkspaceOpen", mock.Anything, mock.Anything).Return(&pb.RpcWorkspaceOpenResponse{
Error: &pb.RpcWorkspaceOpenResponseError{Code: pb.RpcWorkspaceOpenResponseError_NULL},
Info: &model.AccountInfo{
HomeObjectId: "home-object-id",
ArchiveObjectId: "archive-object-id",
ProfileObjectId: "profile-object-id",
MarketplaceWorkspaceId: "marketplace-workspace-id",
WorkspaceObjectId: "workspace-object-id",
DeviceId: "device-id",
AccountSpaceId: "account-space-id",
WidgetsId: "widgets-id",
SpaceViewId: "space-view-id",
TechSpaceId: "tech-space-id",
GatewayUrl: "gateway-url",
LocalStoragePath: "local-storage-path",
TimeZone: "time-zone",
AnalyticsId: "analytics-id",
NetworkId: "network-id",
},
}, nil).Once()
// when
space, err := fx.CreateSpace(nil, "New Space")
// then
require.NoError(t, err)
require.Equal(t, "new-space-id", space.Id)
})
t.Run("failed workspace creation", func(t *testing.T) {
// given
fx := newFixture(t)
fx.mwMock.On("WorkspaceCreate", mock.Anything, mock.Anything).
Return(&pb.RpcWorkspaceCreateResponse{
Error: &pb.RpcWorkspaceCreateResponseError{Code: pb.RpcWorkspaceCreateResponseError_UNKNOWN_ERROR},
}).Once()
// when
space, err := fx.CreateSpace(nil, "New Space")
// then
require.ErrorIs(t, err, ErrFailedCreateSpace)
require.Equal(t, Space{}, space)
})
}
func TestSpaceService_ListMembers(t *testing.T) {
t.Run("successfully get members", func(t *testing.T) {
// given
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("member-1"),
bundle.RelationKeyName.String(): pbtypes.String("John Doe"),
bundle.RelationKeyIconEmoji.String(): pbtypes.String("👤"),
bundle.RelationKeyIdentity.String(): pbtypes.String("AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ"),
bundle.RelationKeyGlobalName.String(): pbtypes.String("john.any"),
},
},
{
Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("member-2"),
bundle.RelationKeyName.String(): pbtypes.String("Jane Doe"),
bundle.RelationKeyIconImage.String(): pbtypes.String(iconImage),
bundle.RelationKeyIdentity.String(): pbtypes.String("AAjLbEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMD4"),
bundle.RelationKeyGlobalName.String(): pbtypes.String("jane.any"),
},
},
},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// when
members, total, hasMore, err := fx.ListMembers(nil, "space-id", offset, limit)
// then
require.NoError(t, err)
require.Len(t, members, 2)
require.Equal(t, "member-1", members[0].Id)
require.Equal(t, "John Doe", members[0].Name)
require.Equal(t, "👤", members[0].Icon)
require.Equal(t, "john.any", members[0].GlobalName)
require.Equal(t, "member-2", members[1].Id)
require.Equal(t, "Jane Doe", members[1].Name)
require.Regexpf(t, regexp.MustCompile(gatewayUrl+`/image/`+iconImage), members[1].Icon, "Icon URL does not match")
require.Equal(t, "jane.any", members[1].GlobalName)
require.Equal(t, 2, total)
require.False(t, hasMore)
})
t.Run("no members found", func(t *testing.T) {
// given
fx := newFixture(t)
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
Return(&pb.RpcObjectSearchResponse{
Records: []*types.Struct{},
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
}).Once()
// when
members, total, hasMore, err := fx.ListMembers(nil, "space-id", offset, limit)
// then
require.NoError(t, err)
require.Len(t, members, 0)
require.Equal(t, 0, total)
require.False(t, hasMore)
})
}

View file

@ -0,0 +1,13 @@
package pagination
type PaginationMeta struct {
Total int `json:"total" example:"1024"` // the total number of items available on that endpoint
Offset int `json:"offset" example:"0"` // the current offset
Limit int `json:"limit" example:"100"` // the current limit
HasMore bool `json:"has_more" example:"true"` // whether there are more items available
}
type PaginatedResponse[T any] struct {
Data []T `json:"data"`
Pagination PaginationMeta `json:"pagination"`
}

View file

@ -0,0 +1,82 @@
package pagination
import (
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
// Config holds pagination configuration options.
type Config struct {
DefaultPage int
DefaultPageSize int
MinPageSize int
MaxPageSize int
}
// New creates a Gin middleware for pagination with the provided Config.
func New(cfg Config) gin.HandlerFunc {
return func(c *gin.Context) {
page := getIntQueryParam(c, "offset", cfg.DefaultPage)
size := getIntQueryParam(c, "limit", cfg.DefaultPageSize)
if size < cfg.MinPageSize || size > cfg.MaxPageSize {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("limit must be between %d and %d", cfg.MinPageSize, cfg.MaxPageSize),
})
return
}
c.Set("offset", page)
c.Set("limit", size)
c.Next()
}
}
// getIntQueryParam retrieves an integer query parameter or falls back to a default value.
func getIntQueryParam(c *gin.Context, key string, defaultValue int) int {
valStr := c.DefaultQuery(key, strconv.Itoa(defaultValue))
val, err := strconv.Atoi(valStr)
if err != nil || val < 0 {
return defaultValue
}
return val
}
// RespondWithPagination sends a paginated JSON response.
func RespondWithPagination[T any](c *gin.Context, statusCode int, data []T, total int, offset int, limit int, hasMore bool) {
c.JSON(statusCode, PaginatedResponse[T]{
Data: data,
Pagination: PaginationMeta{
Total: total,
Offset: offset,
Limit: limit,
HasMore: hasMore,
},
})
}
// Paginate slices the records based on the offset and limit, and determines if more records are available.
func Paginate[T any](records []T, offset int, limit int) ([]T, bool) {
if offset < 0 || limit < 1 {
return []T{}, len(records) > 0
}
total := len(records)
if offset > total {
offset = total
}
end := offset + limit
if end > total {
end = total
}
paginated := records[offset:end]
hasMore := end < total
return paginated, hasMore
}

View file

@ -0,0 +1,254 @@
package pagination
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestNew(t *testing.T) {
commonConfig := Config{
DefaultPage: 0,
DefaultPageSize: 10,
MinPageSize: 1,
MaxPageSize: 50,
}
tests := []struct {
name string
queryParams map[string]string
overrideConfig func(cfg Config) Config
expectedStatus int
expectedOffset int
expectedLimit int
}{
{
name: "Valid offset and limit",
queryParams: map[string]string{
"offset": "10",
"limit": "20",
},
overrideConfig: nil,
expectedStatus: http.StatusOK,
expectedOffset: 10,
expectedLimit: 20,
},
{
name: "Offset missing, use default",
queryParams: map[string]string{
"limit": "20",
},
overrideConfig: nil,
expectedStatus: http.StatusOK,
expectedOffset: 0,
expectedLimit: 20,
},
{
name: "Limit missing, use default",
queryParams: map[string]string{
"offset": "5",
},
overrideConfig: nil,
expectedStatus: http.StatusOK,
expectedOffset: 5,
expectedLimit: 10,
},
{
name: "Limit below minimum",
queryParams: map[string]string{
"offset": "5",
"limit": "0",
},
overrideConfig: nil,
expectedStatus: http.StatusBadRequest,
},
{
name: "Limit above maximum",
queryParams: map[string]string{
"offset": "5",
"limit": "100",
},
overrideConfig: nil,
expectedStatus: http.StatusBadRequest,
},
{
name: "Negative offset, use default",
queryParams: map[string]string{
"offset": "-5",
"limit": "10",
},
overrideConfig: nil,
expectedStatus: http.StatusOK,
expectedOffset: 0,
expectedLimit: 10,
},
{
name: "Custom min and max page size",
queryParams: map[string]string{
"offset": "5",
"limit": "15",
},
overrideConfig: func(cfg Config) Config {
cfg.MinPageSize = 10
cfg.MaxPageSize = 20
return cfg
},
expectedStatus: http.StatusOK,
expectedOffset: 5,
expectedLimit: 15,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Apply overrideConfig if provided
cfg := commonConfig
if tt.overrideConfig != nil {
cfg = tt.overrideConfig(cfg)
}
// Set up Gin
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(New(cfg))
// Define a test endpoint
r.GET("/", func(c *gin.Context) {
offset, _ := c.Get("offset")
limit, _ := c.Get("limit")
c.JSON(http.StatusOK, gin.H{
"offset": offset,
"limit": limit,
})
})
// Create a test request
req := httptest.NewRequest(http.MethodGet, "/", nil)
q := req.URL.Query()
for k, v := range tt.queryParams {
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
// Perform the request
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// Check the response
assert.Equal(t, tt.expectedStatus, w.Code)
if w.Code == http.StatusOK {
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(t, err)
// Validate offset and limit
if offset, ok := resp["offset"].(float64); ok {
assert.Equal(t, float64(tt.expectedOffset), offset)
}
if limit, ok := resp["limit"].(float64); ok {
assert.Equal(t, float64(tt.expectedLimit), limit)
}
}
})
}
}
func TestPaginate(t *testing.T) {
type args struct {
records []int
offset int
limit int
}
tests := []struct {
name string
args args
wantPaginated []int
wantHasMore bool
}{
{
name: "Offset=0, Limit=2 (first two items)",
args: args{
records: []int{1, 2, 3, 4, 5},
offset: 0,
limit: 2,
},
wantPaginated: []int{1, 2},
wantHasMore: true, // items remain: [3,4,5]
},
{
name: "Offset=2, Limit=2 (middle slice)",
args: args{
records: []int{1, 2, 3, 4, 5},
offset: 2,
limit: 2,
},
wantPaginated: []int{3, 4},
wantHasMore: true, // item 5 remains
},
{
name: "Offset=4, Limit=2 (tail of the slice)",
args: args{
records: []int{1, 2, 3, 4, 5},
offset: 4,
limit: 2,
},
wantPaginated: []int{5},
wantHasMore: false,
},
{
name: "Offset > length (should return empty)",
args: args{
records: []int{1, 2, 3, 4, 5},
offset: 10,
limit: 2,
},
wantPaginated: []int{},
wantHasMore: false,
},
{
name: "Limit > length (should return entire slice)",
args: args{
records: []int{1, 2, 3},
offset: 0,
limit: 10,
},
wantPaginated: []int{1, 2, 3},
wantHasMore: false,
},
{
name: "Zero limit (no items returned)",
args: args{
records: []int{1, 2, 3, 4, 5},
offset: 1,
limit: 0,
},
wantPaginated: []int{},
wantHasMore: true, // items remain: [2,3,4,5]
},
{
name: "Negative offset and limit (should return empty)",
args: args{
records: []int{1, 2, 3, 4, 5},
offset: -1,
limit: -1,
},
wantPaginated: []int{},
wantHasMore: true, // items remain: [1,2,3,4,5]
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPaginated, gotHasMore := Paginate(tt.args.records, tt.args.offset, tt.args.limit)
assert.Equal(t, tt.wantPaginated, gotPaginated, "Paginate() gotPaginated = %v, want %v", gotPaginated, tt.wantPaginated)
assert.Equal(t, tt.wantHasMore, gotHasMore, "Paginate() gotHasMore = %v, want %v", gotHasMore, tt.wantHasMore)
})
}
}

View file

@ -0,0 +1,92 @@
package server
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/didip/tollbooth/v8"
"github.com/didip/tollbooth/v8/limiter"
"github.com/gin-gonic/gin"
"github.com/anyproto/anytype-heart/core/anytype/account"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
)
// rateLimit is a middleware that limits the number of requests per second.
func (s *Server) rateLimit(max float64) gin.HandlerFunc {
lmt := tollbooth.NewLimiter(max, nil)
lmt.SetIPLookup(limiter.IPLookup{
Name: "RemoteAddr",
IndexFromRight: 0,
})
return func(c *gin.Context) {
httpError := tollbooth.LimitByRequest(lmt, c.Writer, c.Request)
if httpError != nil {
c.AbortWithStatusJSON(httpError.StatusCode, gin.H{"error": httpError.Message})
return
}
c.Next()
}
}
// ensureAuthenticated is a middleware that ensures the request is authenticated.
func (s *Server) ensureAuthenticated(mw service.ClientCommandsServer) gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization header"})
return
}
if !strings.HasPrefix(authHeader, "Bearer ") {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"})
return
}
key := strings.TrimPrefix(authHeader, "Bearer ")
// Validate the key - if the key exists in the KeyToToken map, it is considered valid.
// Otherwise, attempt to create a new session using the key and add it to the map upon successful validation.
s.mu.Lock()
token, exists := s.KeyToToken[key]
s.mu.Unlock()
if !exists {
response := mw.WalletCreateSession(context.Background(), &pb.RpcWalletCreateSessionRequest{Auth: &pb.RpcWalletCreateSessionRequestAuthOfAppKey{AppKey: key}})
if response.Error.Code != pb.RpcWalletCreateSessionResponseError_NULL {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
return
}
token = response.Token
s.mu.Lock()
s.KeyToToken[key] = token
s.mu.Unlock()
}
// Add token to request context for downstream services (subscriptions, events, etc.)
c.Set("token", token)
c.Next()
}
}
// ensureAccountInfo is a middleware that ensures the account info is available in the services.
func (s *Server) ensureAccountInfo(accountService account.Service) gin.HandlerFunc {
return func(c *gin.Context) {
accInfo, err := accountService.GetInfo(context.Background())
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get account info: %v", err)})
return
}
s.exportService.AccountInfo = accInfo
s.objectService.AccountInfo = accInfo
s.spaceService.AccountInfo = accInfo
s.searchService.AccountInfo = accInfo
c.Next()
}
}

92
core/api/server/router.go Normal file
View file

@ -0,0 +1,92 @@
package server
import (
"os"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
_ "github.com/anyproto/anytype-heart/core/api/docs"
"github.com/anyproto/anytype-heart/core/anytype/account"
"github.com/anyproto/anytype-heart/core/api/internal/auth"
"github.com/anyproto/anytype-heart/core/api/internal/export"
"github.com/anyproto/anytype-heart/core/api/internal/object"
"github.com/anyproto/anytype-heart/core/api/internal/search"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/core/api/pagination"
"github.com/anyproto/anytype-heart/pb/service"
)
const (
defaultPage = 0
defaultPageSize = 100
minPageSize = 1
maxPageSize = 1000
maxWriteRequestsPerSecond = 1
)
// NewRouter builds and returns a *gin.Engine with all routes configured.
func (s *Server) NewRouter(accountService account.Service, mw service.ClientCommandsServer) *gin.Engine {
debug := os.Getenv("ANYTYPE_API_DEBUG") == "1"
if !debug {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
router.Use(gin.Recovery())
if debug {
router.Use(gin.Logger())
}
paginator := pagination.New(pagination.Config{
DefaultPage: defaultPage,
DefaultPageSize: defaultPageSize,
MinPageSize: minPageSize,
MaxPageSize: maxPageSize,
})
// Swagger route
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// Auth routes (no authentication required)
authGroup := router.Group("/v1/auth")
{
authGroup.POST("/display_code", auth.DisplayCodeHandler(s.authService))
authGroup.POST("/token", auth.TokenHandler(s.authService))
}
// API routes
v1 := router.Group("/v1")
v1.Use(paginator)
v1.Use(s.ensureAuthenticated(mw))
v1.Use(s.ensureAccountInfo(accountService))
{
// Export
v1.POST("/spaces/:space_id/objects/:object_id/export/:format", export.GetObjectExportHandler(s.exportService))
// Object
v1.GET("/spaces/:space_id/objects", object.GetObjectsHandler(s.objectService))
v1.GET("/spaces/:space_id/objects/:object_id", object.GetObjectHandler(s.objectService))
v1.DELETE("/spaces/:space_id/objects/:object_id", s.rateLimit(maxWriteRequestsPerSecond), object.DeleteObjectHandler(s.objectService))
v1.POST("/spaces/:space_id/objects", s.rateLimit(maxWriteRequestsPerSecond), object.CreateObjectHandler(s.objectService))
// Search
v1.POST("/search", search.GlobalSearchHandler(s.searchService))
v1.POST("/spaces/:space_id/search", search.SearchHandler(s.searchService))
// Space
v1.GET("/spaces", space.GetSpacesHandler(s.spaceService))
v1.GET("/spaces/:space_id/members", space.GetMembersHandler(s.spaceService))
v1.POST("/spaces", s.rateLimit(maxWriteRequestsPerSecond), space.CreateSpaceHandler(s.spaceService))
// Type
v1.GET("/spaces/:space_id/types", object.GetTypesHandler(s.objectService))
v1.GET("/spaces/:space_id/types/:type_id", object.GetTypeHandler(s.objectService))
v1.GET("/spaces/:space_id/types/:type_id/templates", object.GetTemplatesHandler(s.objectService))
v1.GET("/spaces/:space_id/types/:type_id/templates/:template_id", object.GetTemplateHandler(s.objectService))
}
return router
}

50
core/api/server/server.go Normal file
View file

@ -0,0 +1,50 @@
package server
import (
"sync"
"github.com/gin-gonic/gin"
"github.com/anyproto/anytype-heart/core/anytype/account"
"github.com/anyproto/anytype-heart/core/api/internal/auth"
"github.com/anyproto/anytype-heart/core/api/internal/export"
"github.com/anyproto/anytype-heart/core/api/internal/object"
"github.com/anyproto/anytype-heart/core/api/internal/search"
"github.com/anyproto/anytype-heart/core/api/internal/space"
"github.com/anyproto/anytype-heart/pb/service"
)
// Server wraps the HTTP server and service logic.
type Server struct {
engine *gin.Engine
authService *auth.AuthService
exportService *export.ExportService
objectService *object.ObjectService
spaceService *space.SpaceService
searchService *search.SearchService
mu sync.Mutex
KeyToToken map[string]string // appKey -> token
}
// NewServer constructs a new Server with default config and sets up the routes.
func NewServer(accountService account.Service, mw service.ClientCommandsServer) *Server {
s := &Server{
authService: auth.NewService(mw),
exportService: export.NewService(mw),
spaceService: space.NewService(mw),
}
s.objectService = object.NewService(mw, s.spaceService)
s.searchService = search.NewService(mw, s.spaceService, s.objectService)
s.engine = s.NewRouter(accountService, mw)
s.KeyToToken = make(map[string]string)
return s
}
// Engine returns the underlying gin.Engine.
func (s *Server) Engine() *gin.Engine {
return s.engine
}

135
core/api/service.go Normal file
View file

@ -0,0 +1,135 @@
package api
import (
"context"
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/anytype-heart/core/anytype/account"
"github.com/anyproto/anytype-heart/core/anytype/config"
"github.com/anyproto/anytype-heart/core/api/server"
"github.com/anyproto/anytype-heart/pb/service"
)
const (
CName = "api"
readTimeout = 5 * time.Second
)
var (
mwSrv service.ClientCommandsServer
)
type Service interface {
app.ComponentRunnable
ReassignAddress(ctx context.Context, listenAddr string) (err error)
}
type apiService struct {
srv *server.Server
httpSrv *http.Server
mw service.ClientCommandsServer
accountService account.Service
listenAddr string
lock sync.Mutex
}
func New() Service {
return &apiService{mw: mwSrv}
}
func (s *apiService) Name() (name string) {
return CName
}
// Init initializes the API service.
//
// @title Anytype API
// @version 1.0
// @description This API allows interaction with Anytype resources such as spaces, objects and types.
// @termsOfService https://anytype.io/terms_of_use
// @contact.name Anytype Support
// @contact.url https://anytype.io/contact
// @contact.email support@anytype.io
// @license.name Any Source Available License 1.0
// @license.url https://github.com/anyproto/anytype-ts/blob/main/LICENSE.md
// @host localhost:31009
// @BasePath /v1
// @securityDefinitions.basic BasicAuth
// @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/
func (s *apiService) Init(a *app.App) (err error) {
s.listenAddr = a.MustComponent(config.CName).(*config.Config).JsonApiListenAddr
s.accountService = a.MustComponent(account.CName).(account.Service)
return nil
}
func (s *apiService) Run(ctx context.Context) (err error) {
s.runServer()
return nil
}
func (s *apiService) Close(ctx context.Context) (err error) {
return s.shutdown(ctx)
}
func (s *apiService) runServer() {
s.lock.Lock()
defer s.lock.Unlock()
if s.listenAddr == "" {
// means that API is disabled
return
}
s.srv = server.NewServer(s.accountService, s.mw)
s.httpSrv = &http.Server{
Addr: s.listenAddr,
Handler: s.srv.Engine(),
ReadHeaderTimeout: readTimeout,
}
fmt.Printf("Starting API server on %s\n", s.httpSrv.Addr)
go func() {
if err := s.httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
fmt.Printf("API server ListenAndServe error: %v\n", err)
}
}()
}
func (s *apiService) shutdown(ctx context.Context) (err error) {
if s.httpSrv == nil {
return nil
}
s.lock.Lock()
defer s.lock.Unlock()
// we don't want graceful shutdown here and block the app close
shutdownCtx, cancel := context.WithTimeout(ctx, time.Millisecond)
defer cancel()
if err := s.httpSrv.Shutdown(shutdownCtx); err != nil {
return err
}
return nil
}
func (s *apiService) ReassignAddress(ctx context.Context, listenAddr string) (err error) {
err = s.shutdown(ctx)
if err != nil {
return err
}
s.listenAddr = listenAddr
s.runServer()
return nil
}
func SetMiddlewareParams(mw service.ClientCommandsServer) {
mwSrv = mw
}

111
core/api/util/error.go Normal file
View file

@ -0,0 +1,111 @@
package util
import (
"errors"
"net/http"
)
// 400
type ValidationError struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
// 401
type UnauthorizedError struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
// 403
type ForbiddenError struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
// 404
type NotFoundError struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
// 500
type ServerError struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
type errCodeMapping struct {
target error
code int
}
// ErrToCode just returns a mapping to pair a target error with a code
func ErrToCode(target error, code int) errCodeMapping {
return errCodeMapping{
target: target,
code: code,
}
}
// MapErrorCode checks if err matches any “target” in the mappings,
// returning the first matching code. If none match, returns 500.
func MapErrorCode(err error, mappings ...errCodeMapping) int {
if err == nil {
return http.StatusOK
}
for _, m := range mappings {
if errors.Is(err, m.target) {
return m.code
}
}
return http.StatusInternalServerError
}
// CodeToAPIError returns an instance of the correct struct
// for the given HTTP code, embedding the supplied message.
func CodeToAPIError(code int, message string) any {
switch code {
case http.StatusNotFound:
return NotFoundError{
Error: struct {
Message string `json:"message"`
}{
Message: message,
},
}
case http.StatusUnauthorized:
return UnauthorizedError{
Error: struct {
Message string `json:"message"`
}{
Message: message,
},
}
case http.StatusBadRequest:
return ValidationError{
Error: struct {
Message string `json:"message"`
}{
Message: message,
},
}
default:
return ServerError{
Error: struct {
Message string `json:"message"`
}{
Message: message,
},
}
}
}

90
core/api/util/util.go Normal file
View file

@ -0,0 +1,90 @@
package util
import (
"context"
"errors"
"fmt"
"strings"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pb/service"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
var (
ErrFailedSearchType = errors.New("failed to search for type")
ErrorTypeNotFound = errors.New("type not found")
)
// GetIconFromEmojiOrImage returns the icon to use for the object, which can be either an emoji or an image url
func GetIconFromEmojiOrImage(accountInfo *model.AccountInfo, iconEmoji string, iconImage string) string {
if iconEmoji != "" {
return iconEmoji
}
if iconImage != "" {
return fmt.Sprintf("%s/image/%s", accountInfo.GatewayUrl, iconImage)
}
return ""
}
// ResolveTypeToName resolves the type ID to the name of the type, e.g. "ot-page" to "Page" or "bafyreigyb6l5szohs32ts26ku2j42yd65e6hqy2u3gtzgdwqv6hzftsetu" to "Custom Type"
func ResolveTypeToName(mw service.ClientCommandsServer, spaceId string, typeId string) (typeName string, err error) {
// Can't look up preinstalled types based on relation key, therefore need to use unique key
relKey := bundle.RelationKeyId.String()
if strings.HasPrefix(typeId, "ot-") {
relKey = bundle.RelationKeyUniqueKey.String()
}
// Call ObjectSearch for object of specified type and return the name
resp := mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
Operator: model.BlockContentDataviewFilter_No,
RelationKey: relKey,
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(typeId),
},
},
Keys: []string{bundle.RelationKeyName.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return "", ErrFailedSearchType
}
if len(resp.Records) == 0 {
return "", ErrorTypeNotFound
}
return resp.Records[0].Fields[bundle.RelationKeyName.String()].GetStringValue(), nil
}
func ResolveUniqueKeyToTypeId(mw service.ClientCommandsServer, spaceId string, uniqueKey string) (typeId string, err error) {
// Call ObjectSearch for type with unique key and return the type's ID
resp := mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
SpaceId: spaceId,
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyUniqueKey.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(uniqueKey),
},
},
Keys: []string{bundle.RelationKeyId.String()},
})
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
return "", ErrFailedSearchType
}
if len(resp.Records) == 0 {
return "", ErrorTypeNotFound
}
return resp.Records[0].Fields[bundle.RelationKeyId.String()].GetStringValue(), nil
}

View file

@ -1,9 +1,13 @@
package application
import (
"context"
"errors"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/anytype-heart/core/anytype/config"
"github.com/anyproto/anytype-heart/core/api"
"github.com/anyproto/anytype-heart/pb"
)
@ -24,3 +28,14 @@ func (s *Service) AccountConfigUpdate(req *pb.RpcAccountConfigUpdateRequest) err
}
return nil
}
func (s *Service) AccountChangeJsonApiAddr(ctx context.Context, addr string) error {
s.lock.RLock()
defer s.lock.RUnlock()
if s.app == nil {
return ErrApplicationIsNotRunning
}
apiService := app.MustComponent[api.Service](s.app)
return apiService.ReassignAddress(ctx, addr)
}

View file

@ -12,8 +12,8 @@ import (
"github.com/anyproto/anytype-heart/core/anytype/account"
"github.com/anyproto/anytype-heart/core/anytype/config"
"github.com/anyproto/anytype-heart/core/block"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/block/detailservice"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/domain/objectorigin"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
@ -57,6 +57,9 @@ func (s *Service) AccountCreate(ctx context.Context, req *pb.RpcAccountCreateReq
cfg.NetworkMode = req.NetworkMode
cfg.NetworkCustomConfigFilePath = req.NetworkCustomConfigFilePath
}
if req.JsonApiListenAddr != "" {
cfg.JsonApiListenAddr = req.JsonApiListenAddr
}
comps := []app.Component{
cfg,
anytype.BootstrapWallet(s.rootPath, derivationResult),

View file

@ -31,15 +31,23 @@ var (
ErrNoMnemonicProvided = errors.New("no mnemonic provided")
ErrIncompatibleVersion = errors.New("can't fetch account's data because remote nodes have incompatible protocol version. Please update anytype to the latest version")
ErrAnotherProcessIsRunning = errors.New("another anytype process is running")
ErrFailedToFindAccountInfo = errors.New("failed to find account info")
ErrAccountIsDeleted = errors.New("account is deleted")
ErrAnotherProcessIsRunning = errors.New("another anytype process is running")
ErrFailedToFindAccountInfo = errors.New("failed to find account info")
ErrAccountIsDeleted = errors.New("account is deleted")
ErrAccountStoreIsNotMigrated = errors.New("account store is not migrated")
)
func (s *Service) AccountSelect(ctx context.Context, req *pb.RpcAccountSelectRequest) (*model.Account, error) {
if req.Id == "" {
return nil, ErrEmptyAccountID
}
curMigration := s.migrationManager.getOrCreateMigration(req.RootPath, req.Id)
if !curMigration.successful() {
return nil, ErrAccountStoreIsNotMigrated
}
if s.migrationManager.isRunning() {
return nil, ErrMigrationRunning
}
if runtime.GOOS != "android" && runtime.GOOS != "ios" {
s.traceRecorder.start()
@ -74,10 +82,10 @@ func (s *Service) AccountSelect(ctx context.Context, req *pb.RpcAccountSelectReq
}
metrics.Service.SetWorkingDir(req.RootPath, req.Id)
return s.start(ctx, req.Id, req.RootPath, req.DisableLocalNetworkSync, req.PreferYamuxTransport, req.NetworkMode, req.NetworkCustomConfigFilePath)
return s.start(ctx, req.Id, req.RootPath, req.DisableLocalNetworkSync, req.JsonApiListenAddr, req.PreferYamuxTransport, req.NetworkMode, req.NetworkCustomConfigFilePath)
}
func (s *Service) start(ctx context.Context, id string, rootPath string, disableLocalNetworkSync bool, preferYamux bool, networkMode pb.RpcAccountNetworkMode, networkConfigFilePath string) (*model.Account, error) {
func (s *Service) start(ctx context.Context, id string, rootPath string, disableLocalNetworkSync bool, jsonApiListenAddr string, preferYamux bool, networkMode pb.RpcAccountNetworkMode, networkConfigFilePath string) (*model.Account, error) {
ctx, task := trace2.NewTask(ctx, "application.start")
defer task.End()
@ -108,6 +116,10 @@ func (s *Service) start(ctx context.Context, id string, rootPath string, disable
if disableLocalNetworkSync {
cfg.DontStartLocalNetworkSyncAutomatically = true
}
if jsonApiListenAddr != "" {
cfg.JsonApiListenAddr = jsonApiListenAddr
}
if preferYamux {
cfg.PeferYamuxTransport = true
}

View file

@ -89,7 +89,7 @@ func (s *Service) AccountChangeNetworkConfigAndRestart(ctx context.Context, req
return ErrFailedToStopApplication
}
_, err = s.start(ctx, accountId, rootPath, conf.DontStartLocalNetworkSyncAutomatically, conf.PeferYamuxTransport, req.NetworkMode, req.NetworkCustomConfigFilePath)
_, err = s.start(ctx, accountId, rootPath, conf.DontStartLocalNetworkSyncAutomatically, conf.JsonApiListenAddr, conf.PeferYamuxTransport, req.NetworkMode, req.NetworkCustomConfigFilePath)
return err
}

View file

@ -0,0 +1,218 @@
package application
import (
"context"
"errors"
"os"
"path/filepath"
"sync"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/anytype-heart/core/anytype"
"github.com/anyproto/anytype-heart/core/anytype/config"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/core"
)
var (
ErrAccountNotFound = errors.New("account not found")
ErrMigrationRunning = errors.New("migration is running")
)
func (s *Service) AccountMigrate(ctx context.Context, req *pb.RpcAccountMigrateRequest) error {
if s.rootPath == "" {
s.rootPath = req.RootPath
}
return s.migrationManager.getOrCreateMigration(req.RootPath, req.Id).wait()
}
func (s *Service) AccountMigrateCancel(ctx context.Context, req *pb.RpcAccountMigrateCancelRequest) error {
m := s.migrationManager.getMigration(req.Id)
if m == nil {
return nil
}
m.cancelMigration()
return nil
}
func (s *Service) migrate(ctx context.Context, id string) error {
res, err := core.WalletAccountAt(s.mnemonic, 0)
if err != nil {
return err
}
if _, err := os.Stat(filepath.Join(s.rootPath, id)); err != nil {
if os.IsNotExist(err) {
return ErrAccountNotFound
}
return err
}
cfg := anytype.BootstrapConfig(false, os.Getenv("ANYTYPE_STAGING") == "1")
cfg.PeferYamuxTransport = true
cfg.DisableNetworkIdCheck = true
comps := []app.Component{
cfg,
anytype.BootstrapWallet(s.rootPath, res),
s.eventSender,
}
a := &app.App{}
anytype.BootstrapMigration(a, comps...)
err = a.Start(ctx)
if err != nil {
return err
}
return a.Close(ctx)
}
type migration struct {
mx sync.Mutex
isStarted bool
isFinished bool
ctx context.Context
cancel context.CancelFunc
manager *migrationManager
err error
id string
done chan struct{}
}
func newMigration(m *migrationManager, id string) *migration {
ctx, cancel := context.WithCancel(context.Background())
return &migration{
ctx: ctx,
cancel: cancel,
done: make(chan struct{}),
id: id,
manager: m,
}
}
func newSuccessfulMigration(manager *migrationManager, id string) *migration {
m := newMigration(manager, id)
m.setFinished(nil, false)
return m
}
func (m *migration) setFinished(err error, notify bool) {
m.mx.Lock()
defer m.mx.Unlock()
m.isFinished = true
m.err = err
close(m.done)
if notify {
m.manager.setMigrationRunning(m.id, false)
}
}
func (m *migration) cancelMigration() {
m.cancel()
_ = m.wait()
}
func (m *migration) wait() error {
m.mx.Lock()
if !m.manager.setMigrationRunning(m.id, true) {
m.mx.Unlock()
return ErrMigrationRunning
}
if !m.isStarted {
m.isStarted = true
} else {
m.mx.Unlock()
<-m.done
return m.err
}
m.mx.Unlock()
err := m.manager.service.migrate(m.ctx, m.id)
if err != nil {
m.setFinished(err, true)
return err
}
m.setFinished(nil, true)
return nil
}
func (m *migration) successful() bool {
m.mx.Lock()
defer m.mx.Unlock()
return m.isFinished && m.err == nil
}
func (m *migration) finished() bool {
m.mx.Lock()
defer m.mx.Unlock()
return m.isFinished
}
type migrationManager struct {
migrations map[string]*migration
service *Service
runningMigration string
sync.Mutex
}
func newMigrationManager(s *Service) *migrationManager {
return &migrationManager{
service: s,
}
}
func (m *migrationManager) setMigrationRunning(id string, isRunning bool) bool {
m.Lock()
defer m.Unlock()
if (m.runningMigration != "" && m.runningMigration != id) && isRunning {
return false
}
if m.runningMigration == "" && !isRunning {
panic("migration is not running")
}
if isRunning {
m.runningMigration = id
} else {
m.runningMigration = ""
}
return true
}
func (m *migrationManager) isRunning() bool {
m.Lock()
defer m.Unlock()
return m.runningMigration != ""
}
func (m *migrationManager) getOrCreateMigration(rootPath, id string) *migration {
m.Lock()
defer m.Unlock()
if m.migrations == nil {
m.migrations = make(map[string]*migration)
}
if m.migrations[id] == nil {
sqlitePath := filepath.Join(rootPath, id, config.SpaceStoreSqlitePath)
baderPath := filepath.Join(rootPath, id, config.SpaceStoreBadgerPath)
if anyPathExists([]string{sqlitePath, baderPath}) {
m.migrations[id] = newMigration(m, id)
} else {
m.migrations[id] = newSuccessfulMigration(m, id)
}
}
if m.migrations[id].finished() && !m.migrations[id].successful() {
// resetting migration
m.migrations[id] = newMigration(m, id)
}
return m.migrations[id]
}
func (m *migrationManager) getMigration(id string) *migration {
m.Lock()
defer m.Unlock()
return m.migrations[id]
}
func anyPathExists(paths []string) bool {
for _, path := range paths {
if _, err := os.Stat(path); err == nil {
return true
}
}
return false
}

View file

@ -30,16 +30,20 @@ type Service struct {
eventSender event.Sender
sessions session.Service
traceRecorder *traceRecorder
migrationManager *migrationManager
appAccountStartInProcessCancel context.CancelFunc
appAccountStartInProcessCancelMutex sync.Mutex
}
func New() *Service {
return &Service{
s := &Service{
sessions: session.New(),
traceRecorder: &traceRecorder{},
}
m := newMigrationManager(s)
s.migrationManager = m
return s
}
func (s *Service) GetApp() *app.App {

View file

@ -9,6 +9,7 @@ import (
"github.com/anyproto/anytype-heart/core/session"
walletComp "github.com/anyproto/anytype-heart/core/wallet"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
func (s *Service) CreateSession(req *pb.RpcWalletCreateSessionRequest) (token string, accountId string, err error) {
@ -31,7 +32,8 @@ func (s *Service) CreateSession(req *pb.RpcWalletCreateSessionRequest) (token st
return "", "", err
}
log.Infof("appLink auth %s", appLink.AppName)
token, err := s.sessions.StartSession(s.sessionSigningKey)
token, err := s.sessions.StartSession(s.sessionSigningKey, model.AccountAuthLocalApiScope(appLink.Scope)) // nolint:gosec
if err != nil {
return "", "", err
}
@ -46,7 +48,7 @@ func (s *Service) CreateSession(req *pb.RpcWalletCreateSessionRequest) (token st
if s.mnemonic != mnemonic {
return "", "", errors.Join(ErrBadInput, fmt.Errorf("incorrect mnemonic"))
}
token, err = s.sessions.StartSession(s.sessionSigningKey)
token, err = s.sessions.StartSession(s.sessionSigningKey, model.AccountAuth_Full)
if err != nil {
return "", "", err
}
@ -61,16 +63,16 @@ func (s *Service) CloseSession(req *pb.RpcWalletCloseSessionRequest) error {
return s.sessions.CloseSession(req.Token)
}
func (s *Service) ValidateSessionToken(token string) error {
func (s *Service) ValidateSessionToken(token string) (model.AccountAuthLocalApiScope, error) {
return s.sessions.ValidateToken(s.sessionSigningKey, token)
}
func (s *Service) LinkLocalStartNewChallenge(clientInfo *pb.EventAccountLinkChallengeClientInfo) (id string, err error) {
func (s *Service) LinkLocalStartNewChallenge(scope model.AccountAuthLocalApiScope, clientInfo *pb.EventAccountLinkChallengeClientInfo) (id string, err error) {
if s.app == nil {
return "", ErrApplicationIsNotRunning
}
id, value, err := s.sessions.StartNewChallenge(clientInfo)
id, value, err := s.sessions.StartNewChallenge(scope, clientInfo)
if err != nil {
return "", err
}
@ -78,6 +80,7 @@ func (s *Service) LinkLocalStartNewChallenge(clientInfo *pb.EventAccountLinkChal
AccountLinkChallenge: &pb.EventAccountLinkChallenge{
Challenge: value,
ClientInfo: clientInfo,
Scope: scope,
},
}))
return id, nil
@ -87,7 +90,7 @@ func (s *Service) LinkLocalSolveChallenge(req *pb.RpcAccountLocalLinkSolveChalle
if s.app == nil {
return "", "", ErrApplicationIsNotRunning
}
clientInfo, token, err := s.sessions.SolveChallenge(req.ChallengeId, req.Answer, s.sessionSigningKey)
clientInfo, token, scope, err := s.sessions.SolveChallenge(req.ChallengeId, req.Answer, s.sessionSigningKey)
if err != nil {
return "", "", err
}
@ -96,7 +99,13 @@ func (s *Service) LinkLocalSolveChallenge(req *pb.RpcAccountLocalLinkSolveChalle
AppName: clientInfo.ProcessName,
AppPath: clientInfo.ProcessPath,
CreatedAt: time.Now().Unix(),
Scope: int(scope),
})
s.eventSender.Broadcast(event.NewEventSingleMessage("", &pb.EventMessageValueOfAccountLinkChallengeHide{
AccountLinkChallengeHide: &pb.EventAccountLinkChallengeHide{
Challenge: req.Answer,
},
}))
return
}

View file

@ -6,6 +6,7 @@ package core
import (
"context"
"fmt"
"strings"
"github.com/gogo/protobuf/proto"
"github.com/gogo/protobuf/protoc-gen-gogo/descriptor"
@ -15,8 +16,24 @@ import (
"google.golang.org/grpc/metadata"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
var limitedScopeMethods = map[string]struct{}{
"ObjectSearch": {},
"ObjectShow": {},
"ObjectCreate": {},
"ObjectCreateFromUrl": {},
"BlockPreview": {},
"BlockPaste": {},
"BroadcastPayloadEvent": {},
"AccountSelect": {},
"ListenSessionEvents": {},
"ObjectSearchSubscribe": {},
"ObjectCreateRelationOption": {},
// need to replace with other method to get info
}
func (mw *Middleware) Authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
_, d := descriptor.ForMessage(req.(descriptor.Message))
noAuth := proto.GetBoolExtension(d.GetOptions(), pb.E_NoAuth, false)
@ -35,11 +52,21 @@ func (mw *Middleware) Authorize(ctx context.Context, req interface{}, info *grpc
}
tok := v[0]
err = mw.applicationService.ValidateSessionToken(tok)
var scope model.AccountAuthLocalApiScope
scope, err = mw.applicationService.ValidateSessionToken(tok)
if err != nil {
return nil, status.Error(codes.Unauthenticated, err.Error())
}
switch scope {
case model.AccountAuth_Full:
case model.AccountAuth_Limited:
methodTrimmed := strings.TrimPrefix(info.FullMethod, "/anytype.ClientCommands/")
if _, ok := limitedScopeMethods[methodTrimmed]; !ok {
return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("method %s not allowed for %s", methodTrimmed, scope.String()))
}
default:
return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("method %s not allowed for %s scope", info.FullMethod, scope.String()))
}
resp, err = handler(ctx, req)
return
}

View file

@ -8,7 +8,7 @@ import (
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/app/ocache"
"github.com/cheggaaa/mb"
"github.com/cheggaaa/mb/v3"
"github.com/samber/lo"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
@ -56,10 +56,12 @@ type watcher struct {
resolver idresolver.Resolver
spaceService space.Service
infoBatch *mb.MB
infoBatch *mb.MB[spaceindex.LinksUpdateInfo]
lock sync.Mutex
accumulatedBacklinks map[string]*backLinksUpdate
aggregationInterval time.Duration
cancelCtx context.CancelFunc
ctx context.Context
}
func New() UpdateWatcher {
@ -75,22 +77,26 @@ func (w *watcher) Init(a *app.App) error {
w.store = app.MustComponent[objectstore.ObjectStore](a)
w.resolver = app.MustComponent[idresolver.Resolver](a)
w.spaceService = app.MustComponent[space.Service](a)
w.infoBatch = mb.New(0)
w.infoBatch = mb.New[spaceindex.LinksUpdateInfo](0)
w.accumulatedBacklinks = make(map[string]*backLinksUpdate)
w.aggregationInterval = defaultAggregationInterval
return nil
}
func (w *watcher) Close(context.Context) error {
if w.cancelCtx != nil {
w.cancelCtx()
}
if err := w.infoBatch.Close(); err != nil {
log.Errorf("failed to close message batch: %v", err)
}
return nil
}
func (w *watcher) Run(context.Context) error {
func (w *watcher) Run(ctx context.Context) error {
w.ctx, w.cancelCtx = context.WithCancel(context.Background())
w.updater.SubscribeLinksUpdate(func(info spaceindex.LinksUpdateInfo) {
if err := w.infoBatch.Add(info); err != nil {
if err := w.infoBatch.Add(w.ctx, info); err != nil {
log.With("objectId", info.LinksFromId).Errorf("failed to add backlinks update info to message batch: %v", err)
}
})
@ -165,17 +171,16 @@ func (w *watcher) backlinksUpdateHandler() {
}()
for {
msgs := w.infoBatch.Wait()
msgs, err := w.infoBatch.Wait(w.ctx)
if err != nil {
return
}
if len(msgs) == 0 {
return
}
w.lock.Lock()
for _, msg := range msgs {
info, ok := msg.(spaceindex.LinksUpdateInfo)
if !ok {
continue
}
for _, info := range msgs {
info = cleanSelfLinks(info)
applyUpdates(w.accumulatedBacklinks, info)
}
@ -210,7 +215,7 @@ func (w *watcher) updateBackLinksInObject(id string, backlinksUpdate *backLinksU
log.With("objectId", id).Errorf("failed to resolve space id for object: %v", err)
return
}
spc, err := w.spaceService.Get(context.Background(), spaceId)
spc, err := w.spaceService.Get(w.ctx, spaceId)
if err != nil {
log.With("objectId", id, "spaceId", spaceId).Errorf("failed to get space: %v", err)
return

View file

@ -5,7 +5,7 @@ import (
"time"
"github.com/anyproto/any-sync/app/ocache"
"github.com/cheggaaa/mb"
"github.com/cheggaaa/mb/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -56,7 +56,7 @@ func newFixture(t *testing.T, aggregationInterval time.Duration) *fixture {
spaceService: spaceSvc,
aggregationInterval: aggregationInterval,
infoBatch: mb.New(0),
infoBatch: mb.New[spaceindex.LinksUpdateInfo](0),
accumulatedBacklinks: make(map[string]*backLinksUpdate),
}

View file

@ -22,6 +22,11 @@ type ObjectGetter interface {
GetObjectByFullID(ctx context.Context, id domain.FullID) (sb smartblock.SmartBlock, err error)
}
type CachedObjectGetter interface {
ObjectGetter
TryRemoveFromCache(ctx context.Context, objectId string) (res bool, err error)
}
func Do[t any](p ObjectGetter, objectID string, apply func(sb t) error) error {
ctx := context.Background()
sb, err := p.GetObject(ctx, objectID)

View file

@ -0,0 +1,214 @@
// Code generated by mockery. DO NOT EDIT.
package mock_cache
import (
context "context"
domain "github.com/anyproto/anytype-heart/core/domain"
mock "github.com/stretchr/testify/mock"
smartblock "github.com/anyproto/anytype-heart/core/block/editor/smartblock"
)
// MockCachedObjectGetter is an autogenerated mock type for the CachedObjectGetter type
type MockCachedObjectGetter struct {
mock.Mock
}
type MockCachedObjectGetter_Expecter struct {
mock *mock.Mock
}
func (_m *MockCachedObjectGetter) EXPECT() *MockCachedObjectGetter_Expecter {
return &MockCachedObjectGetter_Expecter{mock: &_m.Mock}
}
// GetObject provides a mock function with given fields: ctx, objectID
func (_m *MockCachedObjectGetter) GetObject(ctx context.Context, objectID string) (smartblock.SmartBlock, error) {
ret := _m.Called(ctx, objectID)
if len(ret) == 0 {
panic("no return value specified for GetObject")
}
var r0 smartblock.SmartBlock
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (smartblock.SmartBlock, error)); ok {
return rf(ctx, objectID)
}
if rf, ok := ret.Get(0).(func(context.Context, string) smartblock.SmartBlock); ok {
r0 = rf(ctx, objectID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(smartblock.SmartBlock)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, objectID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockCachedObjectGetter_GetObject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetObject'
type MockCachedObjectGetter_GetObject_Call struct {
*mock.Call
}
// GetObject is a helper method to define mock.On call
// - ctx context.Context
// - objectID string
func (_e *MockCachedObjectGetter_Expecter) GetObject(ctx interface{}, objectID interface{}) *MockCachedObjectGetter_GetObject_Call {
return &MockCachedObjectGetter_GetObject_Call{Call: _e.mock.On("GetObject", ctx, objectID)}
}
func (_c *MockCachedObjectGetter_GetObject_Call) Run(run func(ctx context.Context, objectID string)) *MockCachedObjectGetter_GetObject_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *MockCachedObjectGetter_GetObject_Call) Return(sb smartblock.SmartBlock, err error) *MockCachedObjectGetter_GetObject_Call {
_c.Call.Return(sb, err)
return _c
}
func (_c *MockCachedObjectGetter_GetObject_Call) RunAndReturn(run func(context.Context, string) (smartblock.SmartBlock, error)) *MockCachedObjectGetter_GetObject_Call {
_c.Call.Return(run)
return _c
}
// GetObjectByFullID provides a mock function with given fields: ctx, id
func (_m *MockCachedObjectGetter) GetObjectByFullID(ctx context.Context, id domain.FullID) (smartblock.SmartBlock, error) {
ret := _m.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for GetObjectByFullID")
}
var r0 smartblock.SmartBlock
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, domain.FullID) (smartblock.SmartBlock, error)); ok {
return rf(ctx, id)
}
if rf, ok := ret.Get(0).(func(context.Context, domain.FullID) smartblock.SmartBlock); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(smartblock.SmartBlock)
}
}
if rf, ok := ret.Get(1).(func(context.Context, domain.FullID) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockCachedObjectGetter_GetObjectByFullID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetObjectByFullID'
type MockCachedObjectGetter_GetObjectByFullID_Call struct {
*mock.Call
}
// GetObjectByFullID is a helper method to define mock.On call
// - ctx context.Context
// - id domain.FullID
func (_e *MockCachedObjectGetter_Expecter) GetObjectByFullID(ctx interface{}, id interface{}) *MockCachedObjectGetter_GetObjectByFullID_Call {
return &MockCachedObjectGetter_GetObjectByFullID_Call{Call: _e.mock.On("GetObjectByFullID", ctx, id)}
}
func (_c *MockCachedObjectGetter_GetObjectByFullID_Call) Run(run func(ctx context.Context, id domain.FullID)) *MockCachedObjectGetter_GetObjectByFullID_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(domain.FullID))
})
return _c
}
func (_c *MockCachedObjectGetter_GetObjectByFullID_Call) Return(sb smartblock.SmartBlock, err error) *MockCachedObjectGetter_GetObjectByFullID_Call {
_c.Call.Return(sb, err)
return _c
}
func (_c *MockCachedObjectGetter_GetObjectByFullID_Call) RunAndReturn(run func(context.Context, domain.FullID) (smartblock.SmartBlock, error)) *MockCachedObjectGetter_GetObjectByFullID_Call {
_c.Call.Return(run)
return _c
}
// TryRemoveFromCache provides a mock function with given fields: ctx, objectId
func (_m *MockCachedObjectGetter) TryRemoveFromCache(ctx context.Context, objectId string) (bool, error) {
ret := _m.Called(ctx, objectId)
if len(ret) == 0 {
panic("no return value specified for TryRemoveFromCache")
}
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok {
return rf(ctx, objectId)
}
if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok {
r0 = rf(ctx, objectId)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, objectId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockCachedObjectGetter_TryRemoveFromCache_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TryRemoveFromCache'
type MockCachedObjectGetter_TryRemoveFromCache_Call struct {
*mock.Call
}
// TryRemoveFromCache is a helper method to define mock.On call
// - ctx context.Context
// - objectId string
func (_e *MockCachedObjectGetter_Expecter) TryRemoveFromCache(ctx interface{}, objectId interface{}) *MockCachedObjectGetter_TryRemoveFromCache_Call {
return &MockCachedObjectGetter_TryRemoveFromCache_Call{Call: _e.mock.On("TryRemoveFromCache", ctx, objectId)}
}
func (_c *MockCachedObjectGetter_TryRemoveFromCache_Call) Run(run func(ctx context.Context, objectId string)) *MockCachedObjectGetter_TryRemoveFromCache_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *MockCachedObjectGetter_TryRemoveFromCache_Call) Return(res bool, err error) *MockCachedObjectGetter_TryRemoveFromCache_Call {
_c.Call.Return(res, err)
return _c
}
func (_c *MockCachedObjectGetter_TryRemoveFromCache_Call) RunAndReturn(run func(context.Context, string) (bool, error)) *MockCachedObjectGetter_TryRemoveFromCache_Call {
_c.Call.Return(run)
return _c
}
// NewMockCachedObjectGetter creates a new instance of MockCachedObjectGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockCachedObjectGetter(t interface {
mock.TestingT
Cleanup(func())
}) *MockCachedObjectGetter {
mock := &MockCachedObjectGetter{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -1,6 +1,7 @@
package block
import (
"context"
"encoding/json"
"fmt"
"net/http"
@ -22,6 +23,7 @@ import (
func (s *Service) DebugRouter(r chi.Router) {
r.Get("/objects", debug.JSONHandler(s.debugListObjects))
r.Get("/tree/{id}", debug.JSONHandler(s.debugTree))
r.Get("/tree_in_space/{spaceId}/{id}", debug.JSONHandler(s.debugTreeInSpace))
r.Get("/objects_per_space/{spaceId}", debug.JSONHandler(s.debugListObjectsPerSpace))
r.Get("/objects/{id}", debug.JSONHandler(s.debugGetObject))
}
@ -122,6 +124,44 @@ func (s *Service) debugTree(req *http.Request) (debugTree, error) {
return result, err
}
// TODO Refactor
func (s *Service) debugTreeInSpace(req *http.Request) (debugTree, error) {
spaceId := chi.URLParam(req, "spaceId")
id := chi.URLParam(req, "id")
result := debugTree{
Id: id,
}
spc, err := s.spaceService.Get(context.Background(), spaceId)
if err != nil {
return result, fmt.Errorf("get space: %w", err)
}
err = spc.Do(id, func(sb smartblock.SmartBlock) error {
ot := sb.Tree()
return ot.IterateRoot(source.UnmarshalChange, func(change *objecttree.Change) bool {
change.Next = nil
raw, err := json.Marshal(change)
if err != nil {
log.Error("debug tree: marshal change", zap.Error(err))
return false
}
ts := time.Unix(change.Timestamp, 0)
ch := debugChange{
Change: raw,
Timestamp: ts.Format(time.RFC3339),
}
if change.Identity != nil {
ch.Identity = change.Identity.Account()
}
result.Changes = append(result.Changes, ch)
return true
})
})
return result, err
}
func (s *Service) getDebugObject(id string) (debugObject, error) {
var obj debugObject
err := cache.Do(s, id, func(sb smartblock.SmartBlock) error {

View file

@ -417,54 +417,6 @@ func (_c *MockService_SetDetails_Call) RunAndReturn(run func(session.Context, st
return _c
}
// SetDetailsAndUpdateLastUsed provides a mock function with given fields: ctx, objectId, details
func (_m *MockService) SetDetailsAndUpdateLastUsed(ctx session.Context, objectId string, details []domain.Detail) error {
ret := _m.Called(ctx, objectId, details)
if len(ret) == 0 {
panic("no return value specified for SetDetailsAndUpdateLastUsed")
}
var r0 error
if rf, ok := ret.Get(0).(func(session.Context, string, []domain.Detail) error); ok {
r0 = rf(ctx, objectId, details)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockService_SetDetailsAndUpdateLastUsed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDetailsAndUpdateLastUsed'
type MockService_SetDetailsAndUpdateLastUsed_Call struct {
*mock.Call
}
// SetDetailsAndUpdateLastUsed is a helper method to define mock.On call
// - ctx session.Context
// - objectId string
// - details []domain.Detail
func (_e *MockService_Expecter) SetDetailsAndUpdateLastUsed(ctx interface{}, objectId interface{}, details interface{}) *MockService_SetDetailsAndUpdateLastUsed_Call {
return &MockService_SetDetailsAndUpdateLastUsed_Call{Call: _e.mock.On("SetDetailsAndUpdateLastUsed", ctx, objectId, details)}
}
func (_c *MockService_SetDetailsAndUpdateLastUsed_Call) Run(run func(ctx session.Context, objectId string, details []domain.Detail)) *MockService_SetDetailsAndUpdateLastUsed_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(session.Context), args[1].(string), args[2].([]domain.Detail))
})
return _c
}
func (_c *MockService_SetDetailsAndUpdateLastUsed_Call) Return(_a0 error) *MockService_SetDetailsAndUpdateLastUsed_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockService_SetDetailsAndUpdateLastUsed_Call) RunAndReturn(run func(session.Context, string, []domain.Detail) error) *MockService_SetDetailsAndUpdateLastUsed_Call {
_c.Call.Return(run)
return _c
}
// SetDetailsList provides a mock function with given fields: ctx, objectIds, details
func (_m *MockService) SetDetailsList(ctx session.Context, objectIds []string, details []domain.Detail) error {
ret := _m.Called(ctx, objectIds, details)

View file

@ -31,7 +31,6 @@ type Service interface {
app.Component
SetDetails(ctx session.Context, objectId string, details []domain.Detail) error
SetDetailsAndUpdateLastUsed(ctx session.Context, objectId string, details []domain.Detail) error
SetDetailsList(ctx session.Context, objectIds []string, details []domain.Detail) error
ModifyDetails(ctx session.Context, objectId string, modifier func(current *domain.Details) (*domain.Details, error)) error
ModifyDetailsList(req *pb.RpcObjectListModifyDetailValuesRequest) error
@ -81,23 +80,10 @@ func (s *service) SetDetails(ctx session.Context, objectId string, details []dom
})
}
func (s *service) SetDetailsAndUpdateLastUsed(ctx session.Context, objectId string, details []domain.Detail) (err error) {
return cache.Do(s.objectGetter, objectId, func(b basic.DetailsSettable) error {
return b.SetDetailsAndUpdateLastUsed(ctx, details, true)
})
}
func (s *service) SetDetailsList(ctx session.Context, objectIds []string, details []domain.Detail) (err error) {
var (
resultError error
anySucceed bool
)
for i, objectId := range objectIds {
setDetailsFunc := s.SetDetails
if i == 0 {
setDetailsFunc = s.SetDetailsAndUpdateLastUsed
}
err := setDetailsFunc(ctx, objectId, details)
func (s *service) SetDetailsList(ctx session.Context, objectIds []string, details []domain.Detail) (resultError error) {
var anySucceed bool
for _, objectId := range objectIds {
err := s.SetDetails(ctx, objectId, details)
if err != nil {
resultError = errors.Join(resultError, err)
} else {
@ -120,20 +106,10 @@ func (s *service) ModifyDetails(ctx session.Context, objectId string, modifier f
})
}
func (s *service) ModifyDetailsAndUpdateLastUsed(ctx session.Context, objectId string, modifier func(current *domain.Details) (*domain.Details, error)) (err error) {
return cache.Do(s.objectGetter, objectId, func(du basic.DetailsUpdatable) error {
return du.UpdateDetailsAndLastUsed(ctx, modifier)
})
}
func (s *service) ModifyDetailsList(req *pb.RpcObjectListModifyDetailValuesRequest) (resultError error) {
var anySucceed bool
for i, objectId := range req.ObjectIds {
modifyDetailsFunc := s.ModifyDetails
if i == 0 {
modifyDetailsFunc = s.ModifyDetailsAndUpdateLastUsed
}
err := modifyDetailsFunc(nil, objectId, func(current *domain.Details) (*domain.Details, error) {
for _, objectId := range req.ObjectIds {
err := s.ModifyDetails(nil, objectId, func(current *domain.Details) (*domain.Details, error) {
for _, op := range req.Operations {
if !pbtypes.IsNullValue(op.Set) {
// Set operation has higher priority than Add and Remove, because it modifies full value

View file

@ -79,7 +79,7 @@ func TestService_SetDetailsList(t *testing.T) {
{Key: bundle.RelationKeyLinkedProjects, Value: domain.StringList([]string{"important", "urgent"})},
}
t.Run("lastUsed is updated once", func(t *testing.T) {
t.Run("no error", func(t *testing.T) {
// given
fx := newFixture(t)
objects := map[string]*smarttest.SmartTest{
@ -98,16 +98,6 @@ func TestService_SetDetailsList(t *testing.T) {
// then
assert.NoError(t, err)
require.Len(t, objects["obj1"].Results.LastUsedUpdates, 3)
assert.Equal(t, []string{
bundle.RelationKeyAssignee.String(),
bundle.RelationKeyDone.String(),
bundle.RelationKeyLinkedProjects.String(),
}, objects["obj1"].Results.LastUsedUpdates)
// lastUsed should be updated only during the work under 1st object
assert.Len(t, objects["obj2"].Results.LastUsedUpdates, 0)
assert.Len(t, objects["obj3"].Results.LastUsedUpdates, 0)
assert.Equal(t, "Mark Twain", objects["obj1"].NewState().Details().GetString(bundle.RelationKeyAssignee))
assert.True(t, objects["obj2"].NewState().Details().GetBool(bundle.RelationKeyDone))
@ -153,7 +143,7 @@ func TestService_ModifyDetailsList(t *testing.T) {
{RelationKey: bundle.RelationKeyDone.String(), Set: domain.Bool(true).ToProto()},
}
t.Run("lastUsed is updated once", func(t *testing.T) {
t.Run("no error", func(t *testing.T) {
fx := newFixture(t)
objects := map[string]*smarttest.SmartTest{
"obj1": smarttest.New("obj1"),
@ -174,11 +164,6 @@ func TestService_ModifyDetailsList(t *testing.T) {
// then
assert.NoError(t, err)
require.Len(t, objects["obj1"].Results.LastUsedUpdates, 3)
// lastUsed should be updated only during the work under 1st object
assert.Len(t, objects["obj2"].Results.LastUsedUpdates, 0)
assert.Len(t, objects["obj3"].Results.LastUsedUpdates, 0)
})
t.Run("some updates failed", func(t *testing.T) {
@ -306,7 +291,7 @@ func TestService_SetWorkspaceDashboardId(t *testing.T) {
assert.Equal(t, wsObjectId, objectId)
ws := &editor.Workspaces{
SmartBlock: sb,
AllOperations: basic.NewBasic(sb, fx.store.SpaceIndex(spaceId), nil, nil, nil),
AllOperations: basic.NewBasic(sb, fx.store.SpaceIndex(spaceId), nil, nil),
}
return ws, nil
})
@ -329,7 +314,7 @@ func TestService_SetWorkspaceDashboardId(t *testing.T) {
assert.Equal(t, wsObjectId, objectId)
ws := &editor.Workspaces{
SmartBlock: sb,
AllOperations: basic.NewBasic(sb, fx.store.SpaceIndex(spaceId), nil, nil, nil),
AllOperations: basic.NewBasic(sb, fx.store.SpaceIndex(spaceId), nil, nil),
}
return ws, nil
})

View file

@ -18,7 +18,6 @@ import (
"github.com/anyproto/anytype-heart/core/block/editor/anystoredebug"
"github.com/anyproto/anytype-heart/core/block/editor/basic"
"github.com/anyproto/anytype-heart/core/block/editor/converter"
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/editor/storestate"
@ -88,23 +87,18 @@ func (a *accountObject) SetDetails(ctx session.Context, details []domain.Detail,
return a.bs.SetDetails(ctx, details, showEvent)
}
func (a *accountObject) SetDetailsAndUpdateLastUsed(ctx session.Context, details []domain.Detail, showEvent bool) (err error) {
return a.bs.SetDetailsAndUpdateLastUsed(ctx, details, showEvent)
}
func New(
sb smartblock.SmartBlock,
keys *accountdata.AccountKeys,
spaceObjects spaceindex.Store,
layoutConverter converter.LayoutConverter,
fileObjectService fileobject.Service,
lastUsedUpdater lastused.ObjectUsageUpdater,
crdtDb anystore.DB,
cfg *config.Config) AccountObject {
return &accountObject{
crdtDb: crdtDb,
keys: keys,
bs: basic.NewBasic(sb, spaceObjects, layoutConverter, fileObjectService, lastUsedUpdater),
bs: basic.NewBasic(sb, spaceObjects, layoutConverter, fileObjectService),
SmartBlock: sb,
cfg: cfg,
relMapper: newRelationsMapper(map[string]KeyType{

View file

@ -55,7 +55,7 @@ func newFixture(t *testing.T, isNewAccount bool, prepareDb func(db anystore.DB))
indexStore := objectstore.NewStoreFixture(t).SpaceIndex("spaceId")
keys, err := accountdata.NewRandom()
require.NoError(t, err)
object := New(sb, keys, indexStore, nil, nil, nil, db, cfg)
object := New(sb, keys, indexStore, nil, nil, db, cfg)
fx := &fixture{
storeFx: objectstore.NewStoreFixture(t),
db: db,

View file

@ -6,7 +6,6 @@ import (
"github.com/samber/lo"
"github.com/anyproto/anytype-heart/core/block/editor/converter"
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/editor/table"
@ -64,12 +63,10 @@ type CommonOperations interface {
type DetailsSettable interface {
SetDetails(ctx session.Context, details []domain.Detail, showEvent bool) (err error)
SetDetailsAndUpdateLastUsed(ctx session.Context, details []domain.Detail, showEvent bool) (err error)
}
type DetailsUpdatable interface {
UpdateDetails(ctx session.Context, update func(current *domain.Details) (*domain.Details, error)) (err error)
UpdateDetailsAndLastUsed(ctx session.Context, update func(current *domain.Details) (*domain.Details, error)) (err error)
}
type Restrictionable interface {
@ -105,14 +102,12 @@ func NewBasic(
objectStore spaceindex.Store,
layoutConverter converter.LayoutConverter,
fileObjectService fileobject.Service,
lastUsedUpdater lastused.ObjectUsageUpdater,
) AllOperations {
return &basic{
SmartBlock: sb,
objectStore: objectStore,
layoutConverter: layoutConverter,
fileObjectService: fileObjectService,
lastUsedUpdater: lastUsedUpdater,
}
}
@ -122,7 +117,6 @@ type basic struct {
objectStore spaceindex.Store
layoutConverter converter.LayoutConverter
fileObjectService fileobject.Service
lastUsedUpdater lastused.ObjectUsageUpdater
}
func (bs *basic) CreateBlock(s *state.State, req pb.RpcBlockCreateRequest) (id string, err error) {

View file

@ -45,7 +45,7 @@ func TestBasic_Create(t *testing.T) {
t.Run("generic", func(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
id, err := b.CreateBlock(st, pb.RpcBlockCreateRequest{
Block: &model.Block{Content: &model.BlockContentOfText{Text: &model.BlockContentText{Text: "ll"}}},
@ -59,7 +59,7 @@ func TestBasic_Create(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
require.NoError(t, smartblock.ObjectApplyTemplate(sb, sb.NewState(), template.WithTitle))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
s := sb.NewState()
id, err := b.CreateBlock(s, pb.RpcBlockCreateRequest{
TargetId: template.TitleBlockId,
@ -80,7 +80,7 @@ func TestBasic_Create(t *testing.T) {
}
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
require.NoError(t, smartblock.ObjectApplyTemplate(sb, sb.NewState(), template.WithTitle))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
_, err := b.CreateBlock(sb.NewState(), pb.RpcBlockCreateRequest{})
assert.ErrorIs(t, err, restriction.ErrRestricted)
})
@ -94,7 +94,7 @@ func TestBasic_Duplicate(t *testing.T) {
AddBlock(simple.New(&model.Block{Id: "3"}))
st := sb.NewState()
newIds, err := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil).Duplicate(st, st, "", 0, []string{"2"})
newIds, err := NewBasic(sb, nil, converter.NewLayoutConverter(), nil).Duplicate(st, st, "", 0, []string{"2"})
require.NoError(t, err)
err = sb.Apply(st)
@ -172,7 +172,7 @@ func TestBasic_Duplicate(t *testing.T) {
ts.SetDetail(bundle.RelationKeySpaceId, domain.String(tc.spaceIds[1]))
// when
newIds, err := NewBasic(source, nil, nil, tc.fos(), nil).Duplicate(ss, ts, "target", model.Block_Inner, []string{"1", "f1"})
newIds, err := NewBasic(source, nil, nil, tc.fos()).Duplicate(ss, ts, "target", model.Block_Inner, []string{"1", "f1"})
require.NoError(t, err)
require.NoError(t, target.Apply(ts))
@ -206,7 +206,7 @@ func TestBasic_Unlink(t *testing.T) {
AddBlock(simple.New(&model.Block{Id: "2", ChildrenIds: []string{"3"}})).
AddBlock(simple.New(&model.Block{Id: "3"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
err := b.Unlink(nil, "2")
require.NoError(t, err)
@ -220,7 +220,7 @@ func TestBasic_Unlink(t *testing.T) {
AddBlock(simple.New(&model.Block{Id: "2", ChildrenIds: []string{"3"}})).
AddBlock(simple.New(&model.Block{Id: "3"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
err := b.Unlink(nil, "2", "3")
require.NoError(t, err)
@ -237,7 +237,7 @@ func TestBasic_Move(t *testing.T) {
AddBlock(simple.New(&model.Block{Id: "3"})).
AddBlock(simple.New(&model.Block{Id: "4"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
err := b.Move(st, st, "4", model.Block_Inner, []string{"3"})
@ -251,7 +251,7 @@ func TestBasic_Move(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
require.NoError(t, smartblock.ObjectApplyTemplate(sb, sb.NewState(), template.WithTitle))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
s := sb.NewState()
id1, err := b.CreateBlock(s, pb.RpcBlockCreateRequest{
TargetId: template.HeaderLayoutId,
@ -300,7 +300,7 @@ func TestBasic_Move(t *testing.T) {
},
),
)
basic := NewBasic(testDoc, nil, converter.NewLayoutConverter(), nil, nil)
basic := NewBasic(testDoc, nil, converter.NewLayoutConverter(), nil)
state := testDoc.NewState()
// when
@ -316,7 +316,7 @@ func TestBasic_Move(t *testing.T) {
AddBlock(newTextBlock("1", "", nil)).
AddBlock(newTextBlock("2", "one", nil))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
err := b.Move(st, st, "1", model.Block_InnerFirst, []string{"2"})
require.NoError(t, err)
@ -336,7 +336,7 @@ func TestBasic_Move(t *testing.T) {
AddBlock(firstBlock).
AddBlock(secondBlock)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
err := b.Move(st, st, "1", model.Block_InnerFirst, []string{"2"})
require.NoError(t, err)
@ -350,7 +350,7 @@ func TestBasic_Move(t *testing.T) {
AddBlock(newTextBlock("1", "", nil)).
AddBlock(newTextBlock("2", "one", nil))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
err := b.Move(st, nil, "1", model.Block_Top, []string{"2"})
require.NoError(t, err)
@ -379,7 +379,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving non-root table block '"+block+"' leads to error", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
// when
@ -394,7 +394,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("no error on moving root table block", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
// when
@ -408,7 +408,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("no error on moving one row between another", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
// when
@ -422,7 +422,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving rows with incorrect position leads to error", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
// when
@ -435,7 +435,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving rows and some other blocks between another leads to error", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
// when
@ -448,7 +448,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving the row between itself leads to error", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
// when
@ -461,7 +461,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving table block from invalid table leads to error", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
st.Unlink("columns")
@ -477,7 +477,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving a block to '"+block+"' block leads to moving it under the table", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
// when
@ -492,7 +492,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
t.Run("moving a block to the invalid table leads to moving it under the table", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
st.Unlink("columns")
@ -516,7 +516,7 @@ func TestBasic_MoveToAnotherObject(t *testing.T) {
sb2 := smarttest.New("test2")
sb2.AddBlock(simple.New(&model.Block{Id: "test2", ChildrenIds: []string{}}))
b := NewBasic(sb1, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb1, nil, converter.NewLayoutConverter(), nil)
srcState := sb1.NewState()
destState := sb2.NewState()
@ -551,7 +551,7 @@ func TestBasic_Replace(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
AddBlock(simple.New(&model.Block{Id: "2"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
newId, err := b.Replace(nil, "2", &model.Block{Content: &model.BlockContentOfText{Text: &model.BlockContentText{Text: "l"}}})
require.NoError(t, err)
require.NotEmpty(t, newId)
@ -561,7 +561,7 @@ func TestBasic_SetFields(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
AddBlock(simple.New(&model.Block{Id: "2"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
fields := &types.Struct{
Fields: map[string]*types.Value{
@ -580,7 +580,7 @@ func TestBasic_Update(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
AddBlock(simple.New(&model.Block{Id: "2"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
err := b.Update(nil, func(b simple.Block) error {
b.Model().BackgroundColor = "test"
@ -594,7 +594,7 @@ func TestBasic_SetDivStyle(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
AddBlock(simple.New(&model.Block{Id: "2", Content: &model.BlockContentOfDiv{Div: &model.BlockContentDiv{}}}))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
err := b.SetDivStyle(nil, model.BlockContentDiv_Dots, "2")
require.NoError(t, err)
@ -614,7 +614,7 @@ func TestBasic_SetRelationKey(t *testing.T) {
t.Run("correct", func(t *testing.T) {
sb := smarttest.New("test")
fillSb(sb)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
err := b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
BlockId: "2",
Key: "testRelKey",
@ -636,7 +636,7 @@ func TestBasic_SetRelationKey(t *testing.T) {
t.Run("not relation block", func(t *testing.T) {
sb := smarttest.New("test")
fillSb(sb)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
require.Error(t, b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
BlockId: "1",
Key: "key",
@ -645,7 +645,7 @@ func TestBasic_SetRelationKey(t *testing.T) {
t.Run("relation not found", func(t *testing.T) {
sb := smarttest.New("test")
fillSb(sb)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
require.Error(t, b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
BlockId: "2",
Key: "not exists",
@ -661,7 +661,7 @@ func TestBasic_FeaturedRelationAdd(t *testing.T) {
s.AddBundledRelationLinks(bundle.RelationKeyDescription)
require.NoError(t, sb.Apply(s))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
newRel := []string{bundle.RelationKeyDescription.String(), bundle.RelationKeyName.String()}
require.NoError(t, b.FeaturedRelationAdd(nil, newRel...))
@ -677,7 +677,7 @@ func TestBasic_FeaturedRelationRemove(t *testing.T) {
template.WithDescription(s)
require.NoError(t, sb.Apply(s))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
require.NoError(t, b.FeaturedRelationRemove(nil, bundle.RelationKeyDescription.String()))
res := sb.NewState()
@ -714,7 +714,7 @@ func TestBasic_ReplaceLink(t *testing.T) {
}
require.NoError(t, sb.Apply(s))
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
require.NoError(t, b.ReplaceLink(oldId, newId))
res := sb.NewState()

View file

@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"strings"
"time"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/state"
@ -28,19 +27,6 @@ func (bs *basic) SetDetails(ctx session.Context, details []domain.Detail, showEv
return err
}
func (bs *basic) SetDetailsAndUpdateLastUsed(ctx session.Context, details []domain.Detail, showEvent bool) (err error) {
var keys []domain.RelationKey
keys, err = bs.setDetails(ctx, details, showEvent)
if err != nil {
return err
}
ts := time.Now().Unix()
for _, key := range keys {
bs.lastUsedUpdater.UpdateLastUsedDate(bs.SpaceID(), key, ts)
}
return nil
}
func (bs *basic) setDetails(ctx session.Context, details []domain.Detail, showEvent bool) (updatedKeys []domain.RelationKey, err error) {
s := bs.NewStateCtx(ctx)
@ -67,23 +53,6 @@ func (bs *basic) UpdateDetails(ctx session.Context, update func(current *domain.
return err
}
func (bs *basic) UpdateDetailsAndLastUsed(ctx session.Context, update func(current *domain.Details) (*domain.Details, error)) error {
oldDetails, newDetails, err := bs.updateDetails(ctx, update)
if err != nil {
return err
}
diff, _ := domain.StructDiff(oldDetails, newDetails)
if diff.Len() == 0 {
return nil
}
ts := time.Now().Unix()
for _, key := range diff.Keys() {
bs.lastUsedUpdater.UpdateLastUsedDate(bs.SpaceID(), key, ts)
}
return nil
}
func (bs *basic) updateDetails(ctx session.Context, update func(current *domain.Details) (*domain.Details, error)) (oldDetails *domain.Details, newDetails *domain.Details, err error) {
if update == nil {
return nil, nil, fmt.Errorf("update function is nil")
@ -180,7 +149,7 @@ func (bs *basic) validateDetailFormat(spaceID string, key domain.RelationKey, v
}
return nil
case model.RelationFormat_status:
vals, ok := v.TryStringList()
vals, ok := v.TryWrapToStringList()
if !ok {
return fmt.Errorf("incorrect type: %v instead of string list", v)
}
@ -190,7 +159,7 @@ func (bs *basic) validateDetailFormat(spaceID string, key domain.RelationKey, v
return bs.validateOptions(r, vals)
case model.RelationFormat_tag:
vals, ok := v.TryStringList()
vals, ok := v.TryWrapToStringList()
if !ok {
return fmt.Errorf("incorrect type: %v instead of string list", v)
}
@ -378,10 +347,6 @@ func (bs *basic) SetObjectTypesInState(s *state.State, objectTypeKeys []domain.T
s.SetObjectTypeKeys(objectTypeKeys)
removeInternalFlags(s)
if bs.CombinedDetails().GetInt64(bundle.RelationKeyOrigin) == int64(model.ObjectOrigin_none) {
bs.lastUsedUpdater.UpdateLastUsedDate(bs.SpaceID(), objectTypeKeys[0], time.Now().Unix())
}
toLayout, err := bs.getLayoutForType(objectTypeKeys[0])
if err != nil {
return fmt.Errorf("get layout for type %s: %w", objectTypeKeys[0], err)

View file

@ -4,10 +4,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/anyproto/anytype-heart/core/block/editor/converter"
"github.com/anyproto/anytype-heart/core/block/editor/lastused/mock_lastused"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
"github.com/anyproto/anytype-heart/core/block/restriction"
"github.com/anyproto/anytype-heart/core/domain"
@ -18,10 +16,9 @@ import (
)
type basicFixture struct {
sb *smarttest.SmartTest
store *spaceindex.StoreFixture
lastUsed *mock_lastused.MockObjectUsageUpdater
basic CommonOperations
sb *smarttest.SmartTest
store *spaceindex.StoreFixture
basic CommonOperations
}
var (
@ -35,15 +32,13 @@ func newBasicFixture(t *testing.T) *basicFixture {
sb.SetSpaceId(spaceId)
store := spaceindex.NewStoreFixture(t)
lastUsed := mock_lastused.NewMockObjectUsageUpdater(t)
b := NewBasic(sb, store, converter.NewLayoutConverter(), nil, lastUsed)
b := NewBasic(sb, store, converter.NewLayoutConverter(), nil)
return &basicFixture{
sb: sb,
store: store,
lastUsed: lastUsed,
basic: b,
sb: sb,
store: store,
basic: b,
}
}
@ -149,7 +144,6 @@ func TestBasic_SetObjectTypesInState(t *testing.T) {
// given
f := newBasicFixture(t)
f.lastUsed.EXPECT().UpdateLastUsedDate(mock.Anything, bundle.TypeKeyTask, mock.Anything).Return().Once()
f.store.AddObjects(t, []objectstore.TestObject{{
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyId: domain.String("ot-task"),

View file

@ -319,7 +319,7 @@ func TestExtractObjects(t *testing.T) {
ObjectTypeUniqueKey: domain.MustUniqueKey(coresb.SmartBlockTypeObjectType, tc.typeKey).Marshal(),
}
ctx := session.NewContext()
linkIds, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil, nil).ExtractBlocksToObjects(ctx, creator, ts, req)
linkIds, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
assert.NoError(t, err)
gotBlockIds := []string{}
@ -374,7 +374,7 @@ func TestExtractObjects(t *testing.T) {
}},
}
ctx := session.NewContext()
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil, nil).ExtractBlocksToObjects(ctx, creator, ts, req)
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
assert.NoError(t, err)
var block *model.Block
for _, block = range sb.Blocks() {
@ -407,7 +407,7 @@ func TestExtractObjects(t *testing.T) {
}},
}
ctx := session.NewContext()
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil, nil).ExtractBlocksToObjects(ctx, creator, ts, req)
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
assert.NoError(t, err)
var addedBlocks []*model.Block
for _, message := range sb.Results.Events {

View file

@ -43,6 +43,7 @@ type StoreObject interface {
ToggleMessageReaction(ctx context.Context, messageId string, emoji string) error
DeleteMessage(ctx context.Context, messageId string) error
SubscribeLastMessages(ctx context.Context, subId string, limit int, asyncInit bool) ([]*model.ChatMessage, int, error)
MarkSeenHeads(heads []string)
Unsubscribe(subId string) error
}
@ -118,6 +119,10 @@ func (s *storeObject) onUpdate() {
s.subscription.flush()
}
func (s *storeObject) MarkSeenHeads(heads []string) {
s.storeSource.MarkSeenHeads(heads)
}
func (s *storeObject) GetMessagesByIds(ctx context.Context, messageIds []string) ([]*model.ChatMessage, error) {
coll, err := s.store.Collection(ctx, collectionName)
if err != nil {

View file

@ -35,7 +35,7 @@ type Dashboard struct {
func NewDashboard(sb smartblock.SmartBlock, objectStore spaceindex.Store, layoutConverter converter.LayoutConverter) *Dashboard {
return &Dashboard{
SmartBlock: sb,
AllOperations: basic.NewBasic(sb, objectStore, layoutConverter, nil, nil),
AllOperations: basic.NewBasic(sb, objectStore, layoutConverter, nil),
Collection: collection.NewCollection(sb, objectStore),
objectStore: objectStore,
}

View file

@ -14,7 +14,6 @@ import (
"github.com/anyproto/anytype-heart/core/block/editor/chatobject"
"github.com/anyproto/anytype-heart/core/block/editor/converter"
"github.com/anyproto/anytype-heart/core/block/editor/file"
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/migration"
"github.com/anyproto/anytype-heart/core/block/object/idresolver"
@ -74,7 +73,6 @@ type ObjectFactory struct {
fileReconciler reconciler.Reconciler
objectDeleter ObjectDeleter
deviceService deviceService
lastUsedUpdater lastused.ObjectUsageUpdater
spaceIdResolver idresolver.Resolver
}
@ -107,7 +105,6 @@ func (f *ObjectFactory) Init(a *app.App) (err error) {
f.objectDeleter = app.MustComponent[ObjectDeleter](a)
f.fileReconciler = app.MustComponent[reconciler.Reconciler](a)
f.deviceService = app.MustComponent[deviceService](a)
f.lastUsedUpdater = app.MustComponent[lastused.ObjectUsageUpdater](a)
f.spaceIdResolver = app.MustComponent[idresolver.Resolver](a)
return nil
}
@ -215,7 +212,7 @@ func (f *ObjectFactory) New(space smartblock.Space, sbType coresb.SmartBlockType
case coresb.SmartBlockTypeChatDerivedObject:
return chatobject.New(sb, f.accountService, f.eventSender, f.objectStore.GetCrdtDb(space.Id()), spaceIndex), nil
case coresb.SmartBlockTypeAccountObject:
return accountobject.New(sb, f.accountService.Keys(), spaceIndex, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater, f.objectStore.GetCrdtDb(space.Id()), f.config), nil
return accountobject.New(sb, f.accountService.Keys(), spaceIndex, f.layoutConverter, f.fileObjectService, f.objectStore.GetCrdtDb(space.Id()), f.config), nil
default:
return nil, fmt.Errorf("unexpected smartblock type: %v", sbType)
}

View file

@ -28,7 +28,7 @@ var fileRequiredRelations = append(pageRequiredRelations, []domain.RelationKey{
func (f *ObjectFactory) newFile(spaceId string, sb smartblock.SmartBlock) *File {
store := f.objectStore.SpaceIndex(spaceId)
basicComponent := basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater)
basicComponent := basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService)
return &File{
SmartBlock: sb,
ChangeReceiver: sb.(source.ChangeReceiver),

View file

@ -1,191 +1,15 @@
package lastused
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/app/logger"
"github.com/cheggaaa/mb/v3"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
coresb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/space"
"github.com/anyproto/anytype-heart/space/clientspace"
)
const (
CName = "object-usage-updater"
maxInstallationTime = 5 * time.Minute
updateInterval = 5 * time.Second
)
type Key interface {
URL() string
String() string
}
type message struct {
spaceId string
key Key
time int64
}
var log = logger.NewNamed("update-last-used-date")
type ObjectUsageUpdater interface {
app.ComponentRunnable
UpdateLastUsedDate(spaceId string, key Key, timeStamp int64)
}
func New() ObjectUsageUpdater {
return &updater{}
}
type updater struct {
store objectstore.ObjectStore
spaceService space.Service
ctx context.Context
cancel context.CancelFunc
msgBatch *mb.MB[message]
}
func (u *updater) Name() string {
return CName
}
func (u *updater) Init(a *app.App) error {
u.store = app.MustComponent[objectstore.ObjectStore](a)
u.spaceService = app.MustComponent[space.Service](a)
u.msgBatch = mb.New[message](0)
return nil
}
func (u *updater) Run(context.Context) error {
u.ctx, u.cancel = context.WithCancel(context.Background())
go u.lastUsedUpdateHandler()
return nil
}
func (u *updater) Close(context.Context) error {
if u.cancel != nil {
u.cancel()
}
if err := u.msgBatch.Close(); err != nil {
log.Error("failed to close message batch", zap.Error(err))
}
return nil
}
func (u *updater) UpdateLastUsedDate(spaceId string, key Key, ts int64) {
if err := u.msgBatch.Add(u.ctx, message{spaceId: spaceId, key: key, time: ts}); err != nil {
log.Error("failed to add last used date info to message batch", zap.Error(err), zap.String("key", key.String()))
}
}
func (u *updater) lastUsedUpdateHandler() {
var (
accumulator = make(map[string]map[Key]int64)
lock sync.Mutex
)
go func() {
for {
select {
case <-u.ctx.Done():
return
case <-time.After(updateInterval):
lock.Lock()
if len(accumulator) == 0 {
lock.Unlock()
continue
}
for spaceId, keys := range accumulator {
log.Debug("updating lastUsedDate for objects in space", zap.Int("objects num", len(keys)), zap.String("spaceId", spaceId))
u.updateLastUsedDateForKeysInSpace(spaceId, keys)
}
accumulator = make(map[string]map[Key]int64)
lock.Unlock()
}
}
}()
for {
msgs, err := u.msgBatch.Wait(u.ctx)
if err != nil {
return
}
lock.Lock()
for _, msg := range msgs {
if keys := accumulator[msg.spaceId]; keys != nil {
keys[msg.key] = msg.time
} else {
keys = map[Key]int64{
msg.key: msg.time,
}
accumulator[msg.spaceId] = keys
}
}
lock.Unlock()
}
}
func (u *updater) updateLastUsedDateForKeysInSpace(spaceId string, keys map[Key]int64) {
spc, err := u.spaceService.Get(u.ctx, spaceId)
if err != nil {
log.Error("failed to get space", zap.String("spaceId", spaceId), zap.Error(err))
return
}
for key, timeStamp := range keys {
if err = u.updateLastUsedDate(spc, key, timeStamp); err != nil {
log.Error("failed to update last used date", zap.String("spaceId", spaceId), zap.String("key", key.String()), zap.Error(err))
}
}
}
func (u *updater) updateLastUsedDate(spc clientspace.Space, key Key, ts int64) error {
uk, err := domain.UnmarshalUniqueKey(key.URL())
if err != nil {
return fmt.Errorf("failed to unmarshall key: %w", err)
}
if uk.SmartblockType() != coresb.SmartBlockTypeObjectType && uk.SmartblockType() != coresb.SmartBlockTypeRelation {
return fmt.Errorf("cannot update lastUsedDate for object with invalid smartBlock type. Only object types and relations are expected")
}
details, err := u.store.SpaceIndex(spc.Id()).GetObjectByUniqueKey(uk)
if err != nil {
return fmt.Errorf("failed to get details: %w", err)
}
id := details.GetString(bundle.RelationKeyId)
if id == "" {
return fmt.Errorf("failed to get id from details: %w", err)
}
if err = spc.DoCtx(u.ctx, id, func(sb smartblock.SmartBlock) error {
st := sb.NewState()
st.SetLocalDetail(bundle.RelationKeyLastUsedDate, domain.Int64(ts))
return sb.Apply(st)
}); err != nil {
return fmt.Errorf("failed to set lastUsedDate to object: %w", err)
}
return nil
}
const maxInstallationTime = 5 * time.Minute
func SetLastUsedDateForInitialObjectType(id string, details *domain.Details) {
if !strings.HasPrefix(id, addr.BundledObjectTypeURLPrefix) || details == nil {

View file

@ -1,22 +1,13 @@
package lastused
import (
"context"
"math/rand"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/space/clientspace/mock_clientspace"
)
func TestSetLastUsedDateForInitialType(t *testing.T) {
@ -56,82 +47,3 @@ func TestSetLastUsedDateForInitialType(t *testing.T) {
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyCollection.BundledURL()], detailMap[bundle.TypeKeyDiaryEntry.BundledURL()]))
})
}
func TestUpdateLastUsedDate(t *testing.T) {
const spaceId = "space"
ts := time.Now().Unix()
isLastUsedDateRecent := func(details *domain.Details, deltaSeconds int64) bool {
return details.GetInt64(bundle.RelationKeyLastUsedDate)+deltaSeconds > time.Now().Unix()
}
store := objectstore.NewStoreFixture(t)
store.AddObjects(t, spaceId, []objectstore.TestObject{
{
bundle.RelationKeyId: domain.String(bundle.RelationKeyCamera.URL()),
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyUniqueKey: domain.String(bundle.RelationKeyCamera.URL()),
},
{
bundle.RelationKeyId: domain.String(bundle.TypeKeyDiaryEntry.URL()),
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyUniqueKey: domain.String(bundle.TypeKeyDiaryEntry.URL()),
},
{
bundle.RelationKeyId: domain.String("rel-custom"),
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyUniqueKey: domain.String("rel-custom"),
},
{
bundle.RelationKeyId: domain.String("opt-done"),
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyUniqueKey: domain.String("opt-done"),
},
})
u := updater{store: store}
getSpace := func() clientspace.Space {
spc := mock_clientspace.NewMockSpace(t)
spc.EXPECT().Id().Return(spaceId)
spc.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, id string, apply func(smartblock.SmartBlock) error) error {
sb := smarttest.New(id)
err := apply(sb)
require.NoError(t, err)
assert.True(t, isLastUsedDateRecent(sb.LocalDetails(), 5))
return nil
})
return spc
}
for _, tc := range []struct {
name string
key Key
getSpace func() clientspace.Space
isErrorExpected bool
}{
{"built-in relation", bundle.RelationKeyCamera, getSpace, false},
{"built-in type", bundle.TypeKeyDiaryEntry, getSpace, false},
{"custom relation", domain.RelationKey("custom"), getSpace, false},
{"option", domain.TypeKey("opt-done"), func() clientspace.Space {
spc := mock_clientspace.NewMockSpace(t)
return spc
}, true},
{"type that is not in store", bundle.TypeKeyAudio, func() clientspace.Space {
spc := mock_clientspace.NewMockSpace(t)
spc.EXPECT().Id().Return(spaceId)
return spc
}, true},
} {
t.Run("update lastUsedDate of "+tc.name, func(t *testing.T) {
err := u.updateLastUsedDate(tc.getSpace(), tc.key, ts)
if tc.isErrorExpected {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

View file

@ -1,258 +0,0 @@
// Code generated by mockery. DO NOT EDIT.
package mock_lastused
import (
context "context"
app "github.com/anyproto/any-sync/app"
lastused "github.com/anyproto/anytype-heart/core/block/editor/lastused"
mock "github.com/stretchr/testify/mock"
)
// MockObjectUsageUpdater is an autogenerated mock type for the ObjectUsageUpdater type
type MockObjectUsageUpdater struct {
mock.Mock
}
type MockObjectUsageUpdater_Expecter struct {
mock *mock.Mock
}
func (_m *MockObjectUsageUpdater) EXPECT() *MockObjectUsageUpdater_Expecter {
return &MockObjectUsageUpdater_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with given fields: ctx
func (_m *MockObjectUsageUpdater) Close(ctx context.Context) error {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for Close")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockObjectUsageUpdater_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockObjectUsageUpdater_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockObjectUsageUpdater_Expecter) Close(ctx interface{}) *MockObjectUsageUpdater_Close_Call {
return &MockObjectUsageUpdater_Close_Call{Call: _e.mock.On("Close", ctx)}
}
func (_c *MockObjectUsageUpdater_Close_Call) Run(run func(ctx context.Context)) *MockObjectUsageUpdater_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockObjectUsageUpdater_Close_Call) Return(err error) *MockObjectUsageUpdater_Close_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockObjectUsageUpdater_Close_Call) RunAndReturn(run func(context.Context) error) *MockObjectUsageUpdater_Close_Call {
_c.Call.Return(run)
return _c
}
// Init provides a mock function with given fields: a
func (_m *MockObjectUsageUpdater) Init(a *app.App) error {
ret := _m.Called(a)
if len(ret) == 0 {
panic("no return value specified for Init")
}
var r0 error
if rf, ok := ret.Get(0).(func(*app.App) error); ok {
r0 = rf(a)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockObjectUsageUpdater_Init_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Init'
type MockObjectUsageUpdater_Init_Call struct {
*mock.Call
}
// Init is a helper method to define mock.On call
// - a *app.App
func (_e *MockObjectUsageUpdater_Expecter) Init(a interface{}) *MockObjectUsageUpdater_Init_Call {
return &MockObjectUsageUpdater_Init_Call{Call: _e.mock.On("Init", a)}
}
func (_c *MockObjectUsageUpdater_Init_Call) Run(run func(a *app.App)) *MockObjectUsageUpdater_Init_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*app.App))
})
return _c
}
func (_c *MockObjectUsageUpdater_Init_Call) Return(err error) *MockObjectUsageUpdater_Init_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockObjectUsageUpdater_Init_Call) RunAndReturn(run func(*app.App) error) *MockObjectUsageUpdater_Init_Call {
_c.Call.Return(run)
return _c
}
// Name provides a mock function with given fields:
func (_m *MockObjectUsageUpdater) Name() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Name")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockObjectUsageUpdater_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
type MockObjectUsageUpdater_Name_Call struct {
*mock.Call
}
// Name is a helper method to define mock.On call
func (_e *MockObjectUsageUpdater_Expecter) Name() *MockObjectUsageUpdater_Name_Call {
return &MockObjectUsageUpdater_Name_Call{Call: _e.mock.On("Name")}
}
func (_c *MockObjectUsageUpdater_Name_Call) Run(run func()) *MockObjectUsageUpdater_Name_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockObjectUsageUpdater_Name_Call) Return(name string) *MockObjectUsageUpdater_Name_Call {
_c.Call.Return(name)
return _c
}
func (_c *MockObjectUsageUpdater_Name_Call) RunAndReturn(run func() string) *MockObjectUsageUpdater_Name_Call {
_c.Call.Return(run)
return _c
}
// Run provides a mock function with given fields: ctx
func (_m *MockObjectUsageUpdater) Run(ctx context.Context) error {
ret := _m.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for Run")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockObjectUsageUpdater_Run_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Run'
type MockObjectUsageUpdater_Run_Call struct {
*mock.Call
}
// Run is a helper method to define mock.On call
// - ctx context.Context
func (_e *MockObjectUsageUpdater_Expecter) Run(ctx interface{}) *MockObjectUsageUpdater_Run_Call {
return &MockObjectUsageUpdater_Run_Call{Call: _e.mock.On("Run", ctx)}
}
func (_c *MockObjectUsageUpdater_Run_Call) Run(run func(ctx context.Context)) *MockObjectUsageUpdater_Run_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *MockObjectUsageUpdater_Run_Call) Return(err error) *MockObjectUsageUpdater_Run_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockObjectUsageUpdater_Run_Call) RunAndReturn(run func(context.Context) error) *MockObjectUsageUpdater_Run_Call {
_c.Call.Return(run)
return _c
}
// UpdateLastUsedDate provides a mock function with given fields: spaceId, key, timeStamp
func (_m *MockObjectUsageUpdater) UpdateLastUsedDate(spaceId string, key lastused.Key, timeStamp int64) {
_m.Called(spaceId, key, timeStamp)
}
// MockObjectUsageUpdater_UpdateLastUsedDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateLastUsedDate'
type MockObjectUsageUpdater_UpdateLastUsedDate_Call struct {
*mock.Call
}
// UpdateLastUsedDate is a helper method to define mock.On call
// - spaceId string
// - key lastused.Key
// - timeStamp int64
func (_e *MockObjectUsageUpdater_Expecter) UpdateLastUsedDate(spaceId interface{}, key interface{}, timeStamp interface{}) *MockObjectUsageUpdater_UpdateLastUsedDate_Call {
return &MockObjectUsageUpdater_UpdateLastUsedDate_Call{Call: _e.mock.On("UpdateLastUsedDate", spaceId, key, timeStamp)}
}
func (_c *MockObjectUsageUpdater_UpdateLastUsedDate_Call) Run(run func(spaceId string, key lastused.Key, timeStamp int64)) *MockObjectUsageUpdater_UpdateLastUsedDate_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(lastused.Key), args[2].(int64))
})
return _c
}
func (_c *MockObjectUsageUpdater_UpdateLastUsedDate_Call) Return() *MockObjectUsageUpdater_UpdateLastUsedDate_Call {
_c.Call.Return()
return _c
}
func (_c *MockObjectUsageUpdater_UpdateLastUsedDate_Call) RunAndReturn(run func(string, lastused.Key, int64)) *MockObjectUsageUpdater_UpdateLastUsedDate_Call {
_c.Call.Return(run)
return _c
}
// NewMockObjectUsageUpdater creates a new instance of MockObjectUsageUpdater. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockObjectUsageUpdater(t interface {
mock.TestingT
Cleanup(func())
}) *MockObjectUsageUpdater {
mock := &MockObjectUsageUpdater{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -59,7 +59,7 @@ func (f *ObjectFactory) newPage(spaceId string, sb smartblock.SmartBlock) *Page
return &Page{
SmartBlock: sb,
ChangeReceiver: sb.(source.ChangeReceiver),
AllOperations: basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater),
AllOperations: basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService),
IHistory: basic.NewHistory(sb),
Text: stext.NewText(
sb,

View file

@ -30,7 +30,7 @@ type participant struct {
}
func (f *ObjectFactory) newParticipant(spaceId string, sb smartblock.SmartBlock, spaceIndex spaceindex.Store) *participant {
basicComponent := basic.NewBasic(sb, spaceIndex, f.layoutConverter, nil, f.lastUsedUpdater)
basicComponent := basic.NewBasic(sb, spaceIndex, f.layoutConverter, nil)
return &participant{
SmartBlock: sb,
DetailsUpdatable: basicComponent,

View file

@ -120,7 +120,7 @@ func TestParticipant_Init(t *testing.T) {
bundle.RelationKeyName: domain.String("test"),
}})
basicComponent := basic.NewBasic(sb, store, nil, nil, nil)
basicComponent := basic.NewBasic(sb, store, nil, nil)
p := &participant{
SmartBlock: sb,
DetailsUpdatable: basicComponent,
@ -147,7 +147,7 @@ func TestParticipant_Init(t *testing.T) {
sb := smarttest.New("root")
store := newStoreFixture(t)
basicComponent := basic.NewBasic(sb, store, nil, nil, nil)
basicComponent := basic.NewBasic(sb, store, nil, nil)
p := &participant{
SmartBlock: sb,
DetailsUpdatable: basicComponent,
@ -196,7 +196,7 @@ func newStoreFixture(t *testing.T) *spaceindex.StoreFixture {
func newParticipantTest(t *testing.T) (*participant, error) {
sb := smarttest.New("root")
store := newStoreFixture(t)
basicComponent := basic.NewBasic(sb, store, nil, nil, nil)
basicComponent := basic.NewBasic(sb, store, nil, nil)
p := &participant{
SmartBlock: sb,
DetailsUpdatable: basicComponent,

View file

@ -40,7 +40,7 @@ func (f *ObjectFactory) newProfile(spaceId string, sb smartblock.SmartBlock) *Pr
fileComponent := file.NewFile(sb, f.fileBlockService, f.picker, f.processService, f.fileUploaderService)
return &Profile{
SmartBlock: sb,
AllOperations: basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater),
AllOperations: basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService),
IHistory: basic.NewHistory(sb),
Text: stext.NewText(
sb,

View file

@ -262,13 +262,6 @@ func (st *SmartTest) SetDetails(ctx session.Context, details []domain.Detail, sh
return
}
func (st *SmartTest) SetDetailsAndUpdateLastUsed(ctx session.Context, details []domain.Detail, showEvent bool) (err error) {
for _, detail := range details {
st.Results.LastUsedUpdates = append(st.Results.LastUsedUpdates, string(detail.Key))
}
return st.SetDetails(ctx, details, showEvent)
}
func (st *SmartTest) UpdateDetails(ctx session.Context, update func(current *domain.Details) (*domain.Details, error)) (err error) {
details := st.Doc.(*state.State).CombinedDetails()
if details == nil {
@ -282,31 +275,6 @@ func (st *SmartTest) UpdateDetails(ctx session.Context, update func(current *dom
return nil
}
func (st *SmartTest) UpdateDetailsAndLastUsed(ctx session.Context, update func(current *domain.Details) (*domain.Details, error)) (err error) {
details := st.Doc.(*state.State).CombinedDetails()
if details == nil {
details = domain.NewDetails()
}
oldDetails := details.Copy()
newDetails, err := update(details)
if err != nil {
return err
}
diff, _ := domain.StructDiff(oldDetails, newDetails)
if diff == nil {
return nil
}
st.Doc.(*state.State).SetDetails(newDetails)
for k, _ := range diff.Iterate() {
st.Results.LastUsedUpdates = append(st.Results.LastUsedUpdates, string(k))
}
return nil
}
func (st *SmartTest) Init(ctx *smartblock.InitContext) (err error) {
if ctx.State == nil {
ctx.State = st.NewState()
@ -453,6 +421,4 @@ func (st *SmartTest) Update(ctx session.Context, apply func(b simple.Block) erro
type Results struct {
Events [][]simple.EventMessage
Applies [][]*model.Block
LastUsedUpdates []string
}

View file

@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
"slices"
"strings"
"time"
@ -136,7 +137,67 @@ type State struct {
groupId string
noObjectType bool
originalCreatedTimestamp int64 // pass here from snapshots when importing objects
originalCreatedTimestamp int64 // pass here from snapshots when importing objects or used for derived objects such as relations, types and etc
}
type RelationsByLayout map[model.ObjectTypeLayout][]domain.RelationKey
type Filters struct {
RelationsWhiteList RelationsByLayout
RemoveBlocks bool
}
// Filter should be called with state copy
func (s *State) Filter(filters *Filters) *State {
if filters == nil {
return s
}
if filters.RemoveBlocks {
s.filterBlocks()
}
if len(filters.RelationsWhiteList) > 0 {
s.filterRelations(filters)
}
return s
}
func (s *State) filterBlocks() {
resultBlocks := make(map[string]simple.Block)
if block, ok := s.blocks[s.rootId]; ok {
resultBlocks[s.rootId] = block
}
s.blocks = resultBlocks
}
func (s *State) filterRelations(filters *Filters) {
resultDetails := domain.NewDetails()
layout, _ := s.Layout()
relationKeys := filters.RelationsWhiteList[layout]
var updatedRelationLinks pbtypes.RelationLinks
for key, value := range s.details.Iterate() {
if slices.Contains(relationKeys, key) {
resultDetails.Set(key, value)
updatedRelationLinks = append(updatedRelationLinks, s.relationLinks.Get(key.String()))
continue
}
}
s.details = resultDetails
if resultDetails.Len() == 0 {
s.details = nil
}
resultLocalDetails := domain.NewDetails()
for key, value := range s.localDetails.Iterate() {
if slices.Contains(relationKeys, key) {
resultLocalDetails.Set(key, value)
updatedRelationLinks = append(updatedRelationLinks, s.relationLinks.Get(key.String()))
continue
}
}
s.localDetails = resultLocalDetails
if resultLocalDetails.Len() == 0 {
s.localDetails = nil
}
s.relationLinks = updatedRelationLinks
}
func (s *State) MigrationVersion() uint32 {
@ -1701,9 +1762,6 @@ func (s *State) AddRelationLinks(links ...*model.RelationLink) {
for _, l := range links {
if !relLinks.Has(l.Key) {
relLinks = append(relLinks, l)
if l.Format == model.RelationFormat_checkbox {
s.SetDetail(domain.RelationKey(l.Key), domain.Bool(false))
}
}
}
s.relationLinks = relLinks

View file

@ -2976,43 +2976,125 @@ func TestState_AddRelationLinks(t *testing.T) {
assert.True(t, s.GetRelationLinks().Has("existingLink"))
assert.Len(t, s.GetRelationLinks(), 1)
})
t.Run("add checkbox link", func(t *testing.T) {
}
func TestFilter(t *testing.T) {
t.Run("remove blocks", func(t *testing.T) {
// given
s := &State{}
checkboxLink := &model.RelationLink{
Key: "checkboxLink",
Format: model.RelationFormat_checkbox,
}
st := NewDoc("root", map[string]simple.Block{
"root": base.NewBase(&model.Block{Id: "root", ChildrenIds: []string{"2"}}),
"2": base.NewBase(&model.Block{Id: "2"}),
}).(*State)
st.AddDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyCoverType: domain.Int64(1),
bundle.RelationKeyName: domain.String("name"),
bundle.RelationKeyAssignee: domain.String("assignee"),
bundle.RelationKeyLayout: domain.Int64(model.ObjectType_todo),
}))
st.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyCoverType.String(),
Format: model.RelationFormat_number,
},
&model.RelationLink{
Key: bundle.RelationKeyName.String(),
Format: model.RelationFormat_longtext,
},
&model.RelationLink{
Key: bundle.RelationKeyAssignee.String(),
Format: model.RelationFormat_object,
},
&model.RelationLink{
Key: bundle.RelationKeyLayout.String(),
Format: model.RelationFormat_number,
},
)
// when
s.AddRelationLinks(checkboxLink)
filteredState := st.Filter(&Filters{RemoveBlocks: true})
// then
relLinks := s.GetRelationLinks()
assert.Equal(t, 1, len(relLinks))
assert.True(t, relLinks.Has("checkboxLink"))
detailValue := s.Details().Get("checkboxLink")
assert.Equal(t, domain.Bool(false), detailValue)
assert.Len(t, filteredState.blocks, 1)
assert.NotNil(t, filteredState.blocks["root"])
})
t.Run("multi links", func(t *testing.T) {
t.Run("filter relations by white list", func(t *testing.T) {
// given
s := &State{}
link1 := &model.RelationLink{
Key: "link1",
Format: model.RelationFormat_shorttext,
}
link2 := &model.RelationLink{
Key: "link2",
Format: model.RelationFormat_checkbox,
}
st := NewDoc("root", map[string]simple.Block{
"root": base.NewBase(&model.Block{Id: "root", ChildrenIds: []string{"2"}}),
"2": base.NewBase(&model.Block{Id: "2"}),
}).(*State)
st.AddDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyCoverType: domain.Int64(1),
bundle.RelationKeyName: domain.String("name"),
bundle.RelationKeyAssignee: domain.String("assignee"),
bundle.RelationKeyLayout: domain.Int64(model.ObjectType_todo),
}))
st.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyCoverType.String(),
Format: model.RelationFormat_number,
},
&model.RelationLink{
Key: bundle.RelationKeyName.String(),
Format: model.RelationFormat_longtext,
},
&model.RelationLink{
Key: bundle.RelationKeyAssignee.String(),
Format: model.RelationFormat_object,
},
&model.RelationLink{
Key: bundle.RelationKeyLayout.String(),
Format: model.RelationFormat_number,
},
)
// when
s.AddRelationLinks(link1, link2)
filteredState := st.Filter(&Filters{RelationsWhiteList: map[model.ObjectTypeLayout][]domain.RelationKey{
model.ObjectType_todo: {bundle.RelationKeyAssignee},
}})
// then
relLinks := s.GetRelationLinks()
assert.Equal(t, 2, len(relLinks))
assert.True(t, relLinks.Has("link1"))
assert.True(t, relLinks.Has("link2"))
assert.Equal(t, filteredState.details.Len(), 1)
assert.Equal(t, filteredState.localDetails.Len(), 0)
assert.Len(t, filteredState.relationLinks, 1)
assert.Equal(t, bundle.RelationKeyAssignee.String(), filteredState.relationLinks[0].Key)
})
t.Run("empty white list relations", func(t *testing.T) {
// given
st := NewDoc("root", map[string]simple.Block{
"root": base.NewBase(&model.Block{Id: "root", ChildrenIds: []string{"2"}}),
"2": base.NewBase(&model.Block{Id: "2"}),
}).(*State)
st.AddDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyCoverType: domain.Int64(1),
bundle.RelationKeyName: domain.String("name"),
bundle.RelationKeyAssignee: domain.String("assignee"),
bundle.RelationKeyLayout: domain.Int64(model.ObjectType_todo),
}))
st.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyCoverType.String(),
Format: model.RelationFormat_number,
},
&model.RelationLink{
Key: bundle.RelationKeyName.String(),
Format: model.RelationFormat_longtext,
},
&model.RelationLink{
Key: bundle.RelationKeyAssignee.String(),
Format: model.RelationFormat_object,
},
&model.RelationLink{
Key: bundle.RelationKeyLayout.String(),
Format: model.RelationFormat_number,
},
)
// when
filteredState := st.Filter(&Filters{RelationsWhiteList: map[model.ObjectTypeLayout][]domain.RelationKey{
model.ObjectType_todo: {},
}})
// then
assert.Equal(t, filteredState.details.Len(), 0)
assert.Equal(t, filteredState.localDetails.Len(), 0)
assert.Len(t, filteredState.relationLinks, 0)
})
}

View file

@ -1,72 +1,60 @@
package debug
import (
"io/ioutil"
"log"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/debug/treearchive"
)
func TestBuildFast(t *testing.T) {
// Specify the directory you want to iterate
dir := "./testdata"
// Read the directory
files, err := ioutil.ReadDir(dir)
if err != nil {
t.Fatalf("Failed to read dir: %s", err)
}
// Iterate over the files
for _, file := range files {
t.Run(file.Name(), func(t *testing.T) {
filePath := filepath.Join(dir, file.Name())
// open the file
f, err := os.Open(filePath)
if err != nil {
t.Fatalf("Failed to open file: %s", err)
}
defer f.Close()
testBuildFast(t, filePath)
})
}
}
func testBuildFast(b *testing.T, filepath string) {
// todo: replace with less heavy tree
archive, err := treearchive.Open(filepath)
if err != nil {
require.NoError(b, err)
}
defer archive.Close()
importer := treearchive.NewTreeImporter(archive.ListStorage(), archive.TreeStorage())
err = importer.Import(false, "")
if err != nil {
log.Fatal("can't import the tree", err)
}
start := time.Now()
_, err = importer.State()
if err != nil {
log.Fatal("can't build state:", err)
}
b.Logf("fast build took %s", time.Since(start))
importer2 := treearchive.NewTreeImporter(archive.ListStorage(), archive.TreeStorage())
err = importer2.Import(false, "")
if err != nil {
log.Fatal("can't import the tree", err)
}
}
// TODO: revive at some point
// func TestBuildFast(t *testing.T) {
// // Specify the directory you want to iterate
// dir := "./testdata"
//
// // Read the directory
// files, err := ioutil.ReadDir(dir)
// if err != nil {
// t.Fatalf("Failed to read dir: %s", err)
// }
//
// // Iterate over the files
// for _, file := range files {
// t.Run(file.Name(), func(t *testing.T) {
// filePath := filepath.Join(dir, file.Name())
//
// // open the file
// f, err := os.Open(filePath)
// if err != nil {
// t.Fatalf("Failed to open file: %s", err)
// }
// defer f.Close()
//
// testBuildFast(t, filePath)
// })
// }
// }
//
// func testBuildFast(b *testing.T, filepath string) {
// // todo: replace with less heavy tree
// archive, err := treearchive.Open(filepath)
// if err != nil {
// require.NoError(b, err)
// }
// defer archive.Close()
//
// importer := exporter.NewTreeImporter(archive.ListStorage(), archive.TreeStorage())
//
// err = importer.Import(false, "")
// if err != nil {
// log.Fatal("can't import the tree", err)
// }
//
// start := time.Now()
// _, err = importer.State()
// if err != nil {
// log.Fatal("can't build state:", err)
// }
// b.Logf("fast build took %s", time.Since(start))
//
// importer2 := exporter.NewTreeImporter(archive.ListStorage(), archive.TreeStorage())
//
// err = importer2.Import(false, "")
// if err != nil {
// log.Fatal("can't import the tree", err)
// }
//
// }

View file

@ -47,10 +47,6 @@ func (stx *StoreStateTx) NextOrder(prev string) string {
return lexId.Next(prev)
}
func (stx *StoreStateTx) NextBeforeOrder(prev string, before string) (string, error) {
return lexId.NextBefore(prev, before)
}
func (stx *StoreStateTx) SetOrder(changeId, order string) (err error) {
stx.arena.Reset()
obj := stx.arena.NewObject()

View file

@ -29,7 +29,7 @@ func NewWidgetObject(
objectStore spaceindex.Store,
layoutConverter converter.LayoutConverter,
) *WidgetObject {
bs := basic.NewBasic(sb, objectStore, layoutConverter, nil, nil)
bs := basic.NewBasic(sb, objectStore, layoutConverter, nil)
return &WidgetObject{
SmartBlock: sb,
Movable: bs,

View file

@ -36,7 +36,7 @@ type Workspaces struct {
func (f *ObjectFactory) newWorkspace(sb smartblock.SmartBlock, store spaceindex.Store) *Workspaces {
w := &Workspaces{
SmartBlock: sb,
AllOperations: basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater),
AllOperations: basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService),
IHistory: basic.NewHistory(sb),
Text: stext.NewText(
sb,

View file

@ -134,51 +134,82 @@ func (e *export) finishWithNotification(spaceId string, exportFormat model.Expor
}, nil)
}
type Doc struct {
Details *domain.Details
isLink bool
}
type Docs map[string]*Doc
func (d Docs) transformToDetailsMap() map[string]*domain.Details {
details := make(map[string]*domain.Details, len(d))
for id, doc := range d {
details[id] = doc.Details
}
return details
}
type exportContext struct {
spaceId string
docs map[string]*domain.Details
includeArchive bool
includeNested bool
includeFiles bool
format model.ExportFormat
isJson bool
reqIds []string
zip bool
path string
spaceId string
docs Docs
includeArchive bool
includeNested bool
includeFiles bool
format model.ExportFormat
isJson bool
reqIds []string
zip bool
path string
linkStateFilters *state.Filters
isLinkProcess bool
includeBackLinks bool
*export
}
func newExportContext(e *export, req pb.RpcObjectListExportRequest) *exportContext {
return &exportContext{
path: req.Path,
spaceId: req.SpaceId,
docs: map[string]*domain.Details{},
includeArchive: req.IncludeArchived,
includeNested: req.IncludeNested,
includeFiles: req.IncludeFiles,
format: req.Format,
isJson: req.IsJson,
reqIds: req.ObjectIds,
zip: req.Zip,
export: e,
ec := &exportContext{
path: req.Path,
spaceId: req.SpaceId,
docs: map[string]*Doc{},
includeArchive: req.IncludeArchived,
includeNested: req.IncludeNested,
includeFiles: req.IncludeFiles,
format: req.Format,
isJson: req.IsJson,
reqIds: req.ObjectIds,
zip: req.Zip,
linkStateFilters: pbFiltersToState(req.LinksStateFilters),
includeBackLinks: req.IncludeBacklinks,
export: e,
}
return ec
}
func (e *exportContext) copy() *exportContext {
return &exportContext{
spaceId: e.spaceId,
docs: e.docs,
includeArchive: e.includeArchive,
includeNested: e.includeNested,
includeFiles: e.includeFiles,
format: e.format,
isJson: e.isJson,
reqIds: e.reqIds,
export: e.export,
spaceId: e.spaceId,
docs: e.docs,
includeArchive: e.includeArchive,
includeNested: e.includeNested,
includeFiles: e.includeFiles,
format: e.format,
isJson: e.isJson,
reqIds: e.reqIds,
export: e.export,
isLinkProcess: e.isLinkProcess,
linkStateFilters: e.linkStateFilters,
includeBackLinks: e.includeBackLinks,
}
}
func (e *exportContext) getStateFilters(id string) *state.Filters {
if doc, ok := e.docs[id]; ok && doc.isLink {
return e.linkStateFilters
}
return nil
}
func (e *exportContext) exportObjects(ctx context.Context, queue process.Queue) (string, int, error) {
var (
err error
@ -261,10 +292,11 @@ func (e *exportContext) exportDocs(ctx context.Context,
succeed *int64,
tasks []process.Task,
) []process.Task {
docsDetails := e.docs.transformToDetailsMap()
for docId := range e.docs {
did := docId
task := func() {
if werr := e.writeDoc(ctx, wr, did); werr != nil {
if werr := e.writeDoc(ctx, wr, did, docsDetails); werr != nil {
log.With("objectID", did).Warnf("can't export doc: %v", werr)
} else {
atomic.AddInt64(succeed, 1)
@ -277,7 +309,7 @@ func (e *exportContext) exportDocs(ctx context.Context,
func (e *exportContext) exportGraphJson(ctx context.Context, succeed int, wr writer, queue process.Queue) int {
mc := graphjson.NewMultiConverter(e.sbtProvider)
mc.SetKnownDocs(e.docs)
mc.SetKnownDocs(e.docs.transformToDetailsMap())
var werr error
if succeed, werr = e.writeMultiDoc(ctx, mc, wr, queue); werr != nil {
log.Warnf("can't export docs: %v", werr)
@ -291,7 +323,7 @@ func (e *exportContext) exportDotAndSVG(ctx context.Context, succeed int, wr wri
format = dot.ExportFormatSVG
}
mc := dot.NewMultiConverter(format, e.sbtProvider)
mc.SetKnownDocs(e.docs)
mc.SetKnownDocs(e.docs.transformToDetailsMap())
var werr error
if succeed, werr = e.writeMultiDoc(ctx, mc, wr, queue); werr != nil {
log.Warnf("can't export docs: %v", werr)
@ -332,7 +364,7 @@ func (e *exportContext) getObjectsByIDs(isProtobuf bool) error {
}
for _, object := range res {
id := object.Details.GetString(bundle.RelationKeyId)
e.docs[id] = object.Details
e.docs[id] = &Doc{Details: object.Details}
}
if isProtobuf {
return e.processProtobuf()
@ -385,7 +417,7 @@ func (e *exportContext) processNotProtobuf() error {
}
if e.includeNested {
for _, id := range ids {
e.addNestedObject(id, map[string]*domain.Details{})
e.addNestedObject(id, map[string]*Doc{})
}
}
return nil
@ -426,7 +458,7 @@ func (e *exportContext) addDependentObjectsFromDataview() error {
err error
)
for id, details := range e.docs {
if isObjectWithDataview(details) {
if isObjectWithDataview(details.Details) {
viewDependentObjectsIds, err = e.getViewDependentObjects(id, viewDependentObjectsIds)
if err != nil {
return err
@ -443,14 +475,17 @@ func (e *exportContext) addDependentObjectsFromDataview() error {
}
for _, object := range append(viewDependentObjects, templates...) {
id := object.Details.GetString(bundle.RelationKeyId)
e.docs[id] = object.Details
e.docs[id] = &Doc{
Details: object.Details,
isLink: e.isLinkProcess,
}
}
return nil
}
func (e *exportContext) getViewDependentObjects(id string, viewDependentObjectsIds []string) ([]string, error) {
err := cache.Do(e.picker, id, func(sb sb.SmartBlock) error {
st := sb.NewState()
st := sb.NewState().Copy().Filter(e.getStateFilters(id))
viewDependentObjectsIds = append(viewDependentObjectsIds, objectlink.DependentObjectIDs(st, sb.Space(), objectlink.Flags{Blocks: true})...)
return nil
})
@ -506,7 +541,7 @@ func (e *exportContext) addDerivedObjects() error {
return nil
}
func (e *exportContext) getRelationsAndTypes(notProcessedObjects map[string]*domain.Details, processedObjects map[string]struct{}) ([]string, []string, []string, error) {
func (e *exportContext) getRelationsAndTypes(notProcessedObjects map[string]*Doc, processedObjects map[string]struct{}) ([]string, []string, []string, error) {
allRelations, allTypes, allSetOfList, err := e.collectDerivedObjects(notProcessedObjects)
if err != nil {
return nil, nil, nil, err
@ -525,11 +560,11 @@ func (e *exportContext) getRelationsAndTypes(notProcessedObjects map[string]*dom
return allRelations, allTypes, allSetOfList, nil
}
func (e *exportContext) collectDerivedObjects(objects map[string]*domain.Details) ([]string, []string, []string, error) {
func (e *exportContext) collectDerivedObjects(objects map[string]*Doc) ([]string, []string, []string, error) {
var relations, objectsTypes, setOf []string
for id := range objects {
err := cache.Do(e.picker, id, func(b sb.SmartBlock) error {
state := b.NewState()
state := b.NewState().Copy().Filter(e.getStateFilters(id))
relations = lo.Union(relations, getObjectRelations(state))
details := state.CombinedDetails()
if isObjectWithDataview(details) {
@ -539,8 +574,10 @@ func (e *exportContext) collectDerivedObjects(objects map[string]*domain.Details
}
relations = lo.Union(relations, dataviewRelations)
}
objectTypeId := details.GetString(bundle.RelationKeyType)
objectsTypes = lo.Union(objectsTypes, []string{objectTypeId})
if details.Has(bundle.RelationKeyType) {
objectTypeId := details.GetString(bundle.RelationKeyType)
objectsTypes = lo.Union(objectsTypes, []string{objectTypeId})
}
setOfList := details.GetStringList(bundle.RelationKeySetOf)
setOf = lo.Union(setOf, setOfList)
return nil
@ -582,7 +619,7 @@ func getDataviewRelations(state *state.State) ([]string, error) {
}
func (e *exportContext) getDerivedObjectsForTypes(allTypes []string, processedObjects map[string]struct{}) ([]string, []string, []string, error) {
notProceedTypes := make(map[string]*domain.Details)
notProceedTypes := make(map[string]*Doc)
var relations, objectTypes []string
for _, object := range allTypes {
if _, ok := processedObjects[object]; ok {
@ -609,12 +646,13 @@ func (e *exportContext) getTemplatesRelationsAndTypes(allTypes []string, process
if len(templates) == 0 {
return nil, nil, nil, nil
}
templatesToProcess := make(map[string]*domain.Details, len(templates))
templatesToProcess := make(map[string]*Doc, len(templates))
for _, template := range templates {
id := template.Details.GetString(bundle.RelationKeyId)
if _, ok := e.docs[id]; !ok {
e.docs[id] = template.Details
templatesToProcess[id] = template.Details
templateDoc := &Doc{Details: template.Details, isLink: e.isLinkProcess}
e.docs[id] = templateDoc
templatesToProcess[id] = templateDoc
}
}
templateRelations, templateType, templateSetOfList, err := e.getRelationsAndTypes(templatesToProcess, processedObjects)
@ -671,7 +709,7 @@ func (e *exportContext) addRelation(relation database.Record) {
relationKey := domain.RelationKey(relation.Details.GetString(bundle.RelationKeyRelationKey))
if relationKey != "" && !bundle.HasRelation(relationKey) {
id := relation.Details.GetString(bundle.RelationKeyId)
e.docs[id] = relation.Details
e.docs[id] = &Doc{Details: relation.Details, isLink: e.isLinkProcess}
}
}
@ -694,7 +732,7 @@ func (e *exportContext) addRelationOptions(relationKey string) error {
}
for _, option := range relationOptions {
id := option.Details.GetString(bundle.RelationKeyId)
e.docs[id] = option.Details
e.docs[id] = &Doc{Details: option.Details, isLink: e.isLinkProcess}
}
return nil
}
@ -748,7 +786,7 @@ func (e *exportContext) addObjectsAndCollectRecommendedRelations(objectTypes []d
return nil, err
}
id := objectTypes[i].Details.GetString(bundle.RelationKeyId)
e.docs[id] = objectTypes[i].Details
e.docs[id] = &Doc{Details: objectTypes[i].Details, isLink: e.isLinkProcess}
if uniqueKey.SmartblockType() == smartblock.SmartBlockTypeObjectType {
key, err := domain.GetTypeKeyFromRawUniqueKey(rawUniqueKey)
if err != nil {
@ -782,13 +820,13 @@ func (e *exportContext) addRecommendedRelations(recommendedRelations []string) e
if bundle.IsSystemRelation(domain.RelationKey(uniqueKey.InternalKey())) {
continue
}
e.docs[id] = relation.Details
e.docs[id] = &Doc{Details: relation.Details, isLink: e.isLinkProcess}
}
return nil
}
func (e *exportContext) addNestedObjects(ids []string) error {
nestedDocs := make(map[string]*domain.Details, 0)
nestedDocs := make(map[string]*Doc, 0)
for _, id := range ids {
e.addNestedObject(id, nestedDocs)
}
@ -798,6 +836,7 @@ func (e *exportContext) addNestedObjects(ids []string) error {
exportCtxChild := e.copy()
exportCtxChild.includeNested = false
exportCtxChild.docs = nestedDocs
exportCtxChild.isLinkProcess = true
err := exportCtxChild.processProtobuf()
if err != nil {
return err
@ -810,10 +849,20 @@ func (e *exportContext) addNestedObjects(ids []string) error {
return nil
}
func (e *exportContext) addNestedObject(id string, nestedDocs map[string]*domain.Details) {
links, err := e.objectStore.SpaceIndex(e.spaceId).GetOutboundLinksById(id)
func (e *exportContext) addNestedObject(id string, nestedDocs map[string]*Doc) {
var links []string
err := cache.Do(e.picker, id, func(sb sb.SmartBlock) error {
st := sb.NewState().Copy().Filter(e.getStateFilters(id))
links = objectlink.DependentObjectIDs(st, sb.Space(), objectlink.Flags{
Blocks: true,
Details: true,
Collection: true,
NoHiddenBundledRelations: true,
NoBackLinks: !e.includeBackLinks,
})
return nil
})
if err != nil {
log.Errorf("export failed to get outbound links for id: %s", err)
return
}
for _, link := range links {
@ -832,8 +881,9 @@ func (e *exportContext) addNestedObject(id string, nestedDocs map[string]*domain
continue
}
if isLinkedObjectExist(rec) {
nestedDocs[link] = rec[0].Details
e.docs[link] = rec[0].Details
exportDoc := &Doc{Details: rec[0].Details, isLink: true}
nestedDocs[link] = exportDoc
e.docs[link] = exportDoc
e.addNestedObject(link, nestedDocs)
}
}
@ -844,7 +894,7 @@ func (e *exportContext) fillLinkedFiles(id string) ([]string, error) {
spaceIndex := e.objectStore.SpaceIndex(e.spaceId)
var fileObjectsIds []string
err := cache.Do(e.picker, id, func(b sb.SmartBlock) error {
b.NewState().IterateLinkedFiles(func(fileObjectId string) {
b.NewState().Copy().Filter(e.getStateFilters(id)).IterateLinkedFiles(func(fileObjectId string) {
res, err := spaceIndex.Query(database.Query{
Filters: []database.FilterRequest{
{
@ -861,7 +911,7 @@ func (e *exportContext) fillLinkedFiles(id string) ([]string, error) {
if len(res) == 0 {
return
}
e.docs[fileObjectId] = res[0].Details
e.docs[fileObjectId] = &Doc{Details: res[0].Details, isLink: e.isLinkProcess}
fileObjectsIds = append(fileObjectsIds, fileObjectId)
})
return nil
@ -885,7 +935,7 @@ func (e *exportContext) getExistedObjects(isProtobuf bool) error {
}
res = append(res, archivedObjects...)
}
e.docs = make(map[string]*domain.Details, len(res))
e.docs = make(map[string]*Doc, len(res))
for _, info := range res {
objectSpaceID := e.spaceId
if objectSpaceID == "" {
@ -899,15 +949,14 @@ func (e *exportContext) getExistedObjects(isProtobuf bool) error {
if !objectValid(sbType, info, e.includeArchive, isProtobuf) {
continue
}
e.docs[info.Id] = info.Details
e.docs[info.Id] = &Doc{Details: info.Details}
}
return nil
}
func (e *exportContext) listTargetTypesFromTemplates(ids []string) []string {
for id, object := range e.docs {
if object.Has(bundle.RelationKeyTargetObjectType) {
if object.Details.Has(bundle.RelationKeyTargetObjectType) {
ids = append(ids, id)
}
}
@ -950,13 +999,14 @@ func (e *exportContext) writeMultiDoc(ctx context.Context, mw converter.MultiCon
return
}
func (e *exportContext) writeDoc(ctx context.Context, wr writer, docId string) (err error) {
func (e *exportContext) writeDoc(ctx context.Context, wr writer, docId string, details map[string]*domain.Details) (err error) {
return cache.Do(e.picker, docId, func(b sb.SmartBlock) error {
st := b.NewState()
if st.CombinedDetails().GetBool(bundle.RelationKeyIsDeleted) {
return nil
}
st = st.Copy().Filter(e.getStateFilters(docId))
if e.includeFiles && b.Type() == smartblock.SmartBlockTypeFileObject {
fileName, err := e.saveFile(ctx, wr, b, e.spaceId == "")
if err != nil {
@ -978,7 +1028,7 @@ func (e *exportContext) writeDoc(ctx context.Context, wr writer, docId string) (
case model.Export_JSON:
conv = pbjson.NewConverter(st)
}
conv.SetKnownDocs(e.docs)
conv.SetKnownDocs(details)
result := conv.Convert(b.Type().ToProto())
var filename string
if e.format == model.Export_Markdown {
@ -1198,7 +1248,7 @@ func cleanupFile(wr writer) {
os.Remove(wr.Path())
}
func listObjectIds(docs map[string]*domain.Details) []string {
func listObjectIds(docs map[string]*Doc) []string {
ids := make([]string, 0, len(docs))
for id := range docs {
ids = append(ids, id)
@ -1209,3 +1259,21 @@ func listObjectIds(docs map[string]*domain.Details) []string {
func isLinkedObjectExist(rec []database.Record) bool {
return len(rec) > 0 && !rec[0].Details.GetBool(bundle.RelationKeyIsDeleted)
}
func pbFiltersToState(filters *pb.RpcObjectListExportStateFilters) *state.Filters {
if filters == nil {
return nil
}
relationByLayoutList := state.RelationsByLayout{}
for _, relationByLayout := range filters.RelationsWhiteList {
allowedRelations := make([]domain.RelationKey, 0, len(relationByLayout.AllowedRelations))
for _, relation := range relationByLayout.AllowedRelations {
allowedRelations = append(allowedRelations, domain.RelationKey(relation))
}
relationByLayoutList[relationByLayout.Layout] = allowedRelations
}
return &state.Filters{
RelationsWhiteList: relationByLayoutList,
RemoveBlocks: filters.RemoveBlocks,
}
}

View file

@ -4,10 +4,12 @@ import (
"archive/zip"
"context"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/anyproto/any-sync/app"
"github.com/gogo/protobuf/jsonpb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@ -361,6 +363,453 @@ func TestExport_Export(t *testing.T) {
assert.NotNil(t, err)
assert.Equal(t, 0, success)
})
t.Run("export with filters success", func(t *testing.T) {
// given
storeFixture := objectstore.NewStoreFixture(t)
objectTypeId := "objectTypeId"
objectTypeUniqueKey, err := domain.NewUniqueKey(smartblock.SmartBlockTypeObjectType, objectTypeId)
assert.Nil(t, err)
objectId := "objectID"
link := "linkId"
storeFixture.AddObjects(t, spaceId, []spaceindex.TestObject{
{
bundle.RelationKeyId: domain.String(link),
bundle.RelationKeyType: domain.String(objectTypeId),
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyDescription: domain.String("description"),
bundle.RelationKeyLayout: domain.Int64(model.ObjectType_set),
bundle.RelationKeyCamera: domain.String("test"),
},
{
bundle.RelationKeyId: domain.String(objectId),
bundle.RelationKeyType: domain.String(objectTypeId),
bundle.RelationKeySpaceId: domain.String(spaceId),
},
{
bundle.RelationKeyId: domain.String(objectTypeId),
bundle.RelationKeyUniqueKey: domain.String(objectTypeUniqueKey.Marshal()),
bundle.RelationKeyLayout: domain.Int64(int64(model.ObjectType_objectType)),
bundle.RelationKeyRecommendedRelations: domain.StringList([]string{addr.MissingObject}),
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyType: domain.String(objectTypeId),
},
})
objectGetter := mock_cache.NewMockObjectGetterComponent(t)
smartBlockTest := smarttest.New(objectId)
doc := smartBlockTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String(objectId),
bundle.RelationKeyType: domain.String(objectTypeId),
}))
doc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
})
smartBlockTest.Doc = doc
smartBlockTest.AddBlock(simple.New(&model.Block{Id: objectId, ChildrenIds: []string{"linkBlock"}, Content: &model.BlockContentOfSmartblock{Smartblock: &model.BlockContentSmartblock{}}}))
smartBlockTest.AddBlock(simple.New(&model.Block{Id: "linkBlock", Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{TargetBlockId: link}}}))
objectType := smarttest.New(objectTypeId)
objectTypeDoc := objectType.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String(objectTypeId),
bundle.RelationKeyType: domain.String(objectTypeId),
}))
objectTypeDoc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
})
objectType.Doc = objectTypeDoc
objectType.SetType(smartblock.SmartBlockTypeObjectType)
linkObject := smarttest.New(link)
linkObjectDoc := linkObject.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String(link),
bundle.RelationKeyType: domain.String(objectTypeId),
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyDescription: domain.String("description"),
bundle.RelationKeyLayout: domain.Int64(model.ObjectType_set),
bundle.RelationKeyCamera: domain.String("test"),
}))
linkObjectDoc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeySpaceId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyDescription.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyLayout.String(),
Format: model.RelationFormat_number,
}, &model.RelationLink{
Key: bundle.RelationKeyCamera.String(),
Format: model.RelationFormat_longtext,
})
linkObject.Doc = linkObjectDoc
linkObject.AddBlock(simple.New(&model.Block{Id: objectId, ChildrenIds: []string{"linkBlock"}, Content: &model.BlockContentOfSmartblock{Smartblock: &model.BlockContentSmartblock{}}}))
linkObject.AddBlock(simple.New(&model.Block{Id: "linkBlock", Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{TargetBlockId: "link1"}}}))
objectGetter.EXPECT().GetObject(context.Background(), objectId).Return(smartBlockTest, nil).Times(4)
objectGetter.EXPECT().GetObject(context.Background(), objectTypeId).Return(objectType, nil)
objectGetter.EXPECT().GetObject(context.Background(), link).Return(linkObject, nil)
a := &app.App{}
mockSender := mock_event.NewMockSender(t)
mockSender.EXPECT().Broadcast(mock.Anything).Return()
a.Register(testutil.PrepareMock(context.Background(), a, mockSender))
service := process.New()
err = service.Init(a)
assert.Nil(t, err)
notifications := mock_notifications.NewMockNotifications(t)
notificationSend := make(chan struct{})
notifications.EXPECT().CreateAndSend(mock.Anything).RunAndReturn(func(notification *model.Notification) error {
close(notificationSend)
return nil
})
provider := mock_typeprovider.NewMockSmartBlockTypeProvider(t)
provider.EXPECT().Type(spaceId, link).Return(smartblock.SmartBlockTypePage, nil)
e := &export{
objectStore: storeFixture,
picker: objectGetter,
processService: service,
notificationService: notifications,
sbtProvider: provider,
}
// when
path, success, err := e.Export(context.Background(), pb.RpcObjectListExportRequest{
SpaceId: spaceId,
Path: t.TempDir(),
ObjectIds: []string{objectId},
Format: model.Export_Protobuf,
Zip: true,
IncludeNested: true,
IncludeFiles: true,
IsJson: true,
LinksStateFilters: &pb.RpcObjectListExportStateFilters{
RelationsWhiteList: []*pb.RpcObjectListExportRelationsWhiteList{
{
Layout: model.ObjectType_set,
AllowedRelations: []string{bundle.RelationKeyCamera.String()},
},
},
RemoveBlocks: true,
},
})
// then
<-notificationSend
assert.Nil(t, err)
assert.Equal(t, 3, success)
reader, err := zip.OpenReader(path)
assert.Nil(t, err)
assert.Len(t, reader.File, 3)
fileNames := make(map[string]bool, 3)
for _, file := range reader.File {
fileNames[file.Name] = true
}
objectPath := filepath.Join(objectsDirectory, link+".pb.json")
assert.True(t, fileNames[objectPath])
file, err := os.Open(objectPath)
if err != nil {
return
}
var sn *pb.SnapshotWithType
err = jsonpb.Unmarshal(file, sn)
assert.Nil(t, err)
assert.Len(t, sn.GetSnapshot().GetData().GetBlocks(), 1)
assert.Equal(t, link, sn.GetSnapshot().GetData().GetBlocks()[0].GetId())
assert.Len(t, sn.GetSnapshot().GetData().GetDetails().GetFields(), 1)
assert.NotNil(t, sn.GetSnapshot().GetData().GetDetails().GetFields()[bundle.RelationKeyCamera.String()])
assert.Len(t, sn.GetSnapshot().GetData().GetRelationLinks(), 1)
assert.Equal(t, bundle.RelationKeyCamera.String(), sn.GetSnapshot().GetData().GetRelationLinks()[0].Key)
})
t.Run("export with backlinks", func(t *testing.T) {
// given
storeFixture := objectstore.NewStoreFixture(t)
objectTypeId := "objectTypeId"
objectTypeUniqueKey, err := domain.NewUniqueKey(smartblock.SmartBlockTypeObjectType, objectTypeId)
assert.Nil(t, err)
objectId := "objectID"
link1 := "linkId"
storeFixture.AddObjects(t, spaceId, []spaceindex.TestObject{
{
bundle.RelationKeyId: domain.String(link1),
bundle.RelationKeyType: domain.String(objectTypeId),
bundle.RelationKeySpaceId: domain.String(spaceId),
},
{
bundle.RelationKeyId: domain.String(objectId),
bundle.RelationKeyType: domain.String(objectTypeId),
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyBacklinks: domain.StringList([]string{link1}),
},
{
bundle.RelationKeyId: domain.String(objectTypeId),
bundle.RelationKeyUniqueKey: domain.String(objectTypeUniqueKey.Marshal()),
bundle.RelationKeyLayout: domain.Int64(int64(model.ObjectType_objectType)),
bundle.RelationKeyRecommendedRelations: domain.StringList([]string{addr.MissingObject}),
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyType: domain.String(objectTypeId),
},
})
objectGetter := mock_cache.NewMockObjectGetterComponent(t)
smartBlockTest := smarttest.New(objectId)
doc := smartBlockTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String(objectId),
bundle.RelationKeyType: domain.String(objectTypeId),
bundle.RelationKeyBacklinks: domain.StringList([]string{link1}),
}))
doc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyBacklinks.String(),
Format: model.RelationFormat_object,
})
smartBlockTest.Doc = doc
objectType := smarttest.New(objectTypeId)
objectTypeDoc := objectType.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String(objectTypeId),
bundle.RelationKeyType: domain.String(objectTypeId),
}))
objectTypeDoc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
})
objectType.Doc = objectTypeDoc
objectType.SetType(smartblock.SmartBlockTypeObjectType)
linkObject := smarttest.New(link1)
linkObjectDoc := linkObject.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String(link1),
bundle.RelationKeyType: domain.String(objectTypeId),
bundle.RelationKeySpaceId: domain.String(spaceId),
}))
linkObjectDoc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeySpaceId.String(),
Format: model.RelationFormat_longtext,
})
linkObject.Doc = linkObjectDoc
objectGetter.EXPECT().GetObject(context.Background(), objectId).Return(smartBlockTest, nil).Times(4)
objectGetter.EXPECT().GetObject(context.Background(), objectTypeId).Return(objectType, nil)
objectGetter.EXPECT().GetObject(context.Background(), link1).Return(linkObject, nil)
a := &app.App{}
mockSender := mock_event.NewMockSender(t)
mockSender.EXPECT().Broadcast(mock.Anything).Return()
a.Register(testutil.PrepareMock(context.Background(), a, mockSender))
service := process.New()
err = service.Init(a)
assert.Nil(t, err)
notifications := mock_notifications.NewMockNotifications(t)
notificationSend := make(chan struct{})
notifications.EXPECT().CreateAndSend(mock.Anything).RunAndReturn(func(notification *model.Notification) error {
close(notificationSend)
return nil
})
provider := mock_typeprovider.NewMockSmartBlockTypeProvider(t)
provider.EXPECT().Type(spaceId, link1).Return(smartblock.SmartBlockTypePage, nil)
e := &export{
objectStore: storeFixture,
picker: objectGetter,
processService: service,
notificationService: notifications,
sbtProvider: provider,
}
// when
path, success, err := e.Export(context.Background(), pb.RpcObjectListExportRequest{
SpaceId: spaceId,
Path: t.TempDir(),
ObjectIds: []string{objectId},
Format: model.Export_Protobuf,
Zip: true,
IncludeNested: true,
IncludeFiles: true,
IsJson: true,
IncludeBacklinks: true,
})
// then
<-notificationSend
assert.Nil(t, err)
assert.Equal(t, 3, success)
reader, err := zip.OpenReader(path)
assert.Nil(t, err)
assert.Len(t, reader.File, 3)
fileNames := make(map[string]bool, 3)
for _, file := range reader.File {
fileNames[file.Name] = true
}
objectPath := filepath.Join(objectsDirectory, link1+".pb.json")
assert.True(t, fileNames[objectPath])
})
t.Run("export without backlinks", func(t *testing.T) {
// given
storeFixture := objectstore.NewStoreFixture(t)
objectTypeId := "objectTypeId"
objectTypeUniqueKey, err := domain.NewUniqueKey(smartblock.SmartBlockTypeObjectType, objectTypeId)
assert.Nil(t, err)
objectId := "objectID"
link1 := "linkId"
storeFixture.AddObjects(t, spaceId, []spaceindex.TestObject{
{
bundle.RelationKeyId: domain.String(link1),
bundle.RelationKeyType: domain.String(objectTypeId),
bundle.RelationKeySpaceId: domain.String(spaceId),
},
{
bundle.RelationKeyId: domain.String(objectId),
bundle.RelationKeyType: domain.String(objectTypeId),
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyBacklinks: domain.StringList([]string{link1}),
},
{
bundle.RelationKeyId: domain.String(objectTypeId),
bundle.RelationKeyUniqueKey: domain.String(objectTypeUniqueKey.Marshal()),
bundle.RelationKeyLayout: domain.Int64(int64(model.ObjectType_objectType)),
bundle.RelationKeyRecommendedRelations: domain.StringList([]string{addr.MissingObject}),
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyType: domain.String(objectTypeId),
},
})
objectGetter := mock_cache.NewMockObjectGetterComponent(t)
smartBlockTest := smarttest.New(objectId)
doc := smartBlockTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String(objectId),
bundle.RelationKeyType: domain.String(objectTypeId),
bundle.RelationKeyBacklinks: domain.StringList([]string{link1}),
}))
doc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyBacklinks.String(),
Format: model.RelationFormat_object,
})
smartBlockTest.Doc = doc
objectType := smarttest.New(objectTypeId)
objectTypeDoc := objectType.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String(objectTypeId),
bundle.RelationKeyType: domain.String(objectTypeId),
}))
objectTypeDoc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
})
objectType.Doc = objectTypeDoc
objectType.SetType(smartblock.SmartBlockTypeObjectType)
objectGetter.EXPECT().GetObject(context.Background(), objectId).Return(smartBlockTest, nil).Times(4)
objectGetter.EXPECT().GetObject(context.Background(), objectTypeId).Return(objectType, nil)
a := &app.App{}
mockSender := mock_event.NewMockSender(t)
mockSender.EXPECT().Broadcast(mock.Anything).Return()
a.Register(testutil.PrepareMock(context.Background(), a, mockSender))
service := process.New()
err = service.Init(a)
assert.Nil(t, err)
notifications := mock_notifications.NewMockNotifications(t)
notificationSend := make(chan struct{})
notifications.EXPECT().CreateAndSend(mock.Anything).RunAndReturn(func(notification *model.Notification) error {
close(notificationSend)
return nil
})
provider := mock_typeprovider.NewMockSmartBlockTypeProvider(t)
e := &export{
objectStore: storeFixture,
picker: objectGetter,
processService: service,
notificationService: notifications,
sbtProvider: provider,
}
// when
path, success, err := e.Export(context.Background(), pb.RpcObjectListExportRequest{
SpaceId: spaceId,
Path: t.TempDir(),
ObjectIds: []string{objectId},
Format: model.Export_Protobuf,
Zip: true,
IncludeNested: true,
IncludeFiles: true,
IsJson: true,
IncludeBacklinks: false,
})
// then
<-notificationSend
assert.Nil(t, err)
assert.Equal(t, 2, success)
reader, err := zip.OpenReader(path)
assert.Nil(t, err)
fileNames := make(map[string]bool, 2)
for _, file := range reader.File {
fileNames[file.Name] = true
}
objectPath := filepath.Join(objectsDirectory, link1+".pb.json")
assert.False(t, fileNames[objectPath])
})
}
func Test_docsForExport(t *testing.T) {
@ -379,14 +828,49 @@ func Test_docsForExport(t *testing.T) {
bundle.RelationKeySpaceId: domain.String(spaceId),
},
})
err := storeFixture.SpaceIndex(spaceId).UpdateObjectLinks(context.Background(), "id", []string{"id1"})
assert.Nil(t, err)
provider := mock_typeprovider.NewMockSmartBlockTypeProvider(t)
provider.EXPECT().Type(spaceId, "id1").Return(smartblock.SmartBlockTypePage, nil)
objectGetter := mock_cache.NewMockObjectGetterComponent(t)
smartBlockTest := smarttest.New("id")
doc := smartBlockTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String("id"),
bundle.RelationKeyType: domain.String("objectTypeId"),
}))
doc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
})
smartBlockTest.Doc = doc
smartBlockTest.AddBlock(simple.New(&model.Block{Id: "id", ChildrenIds: []string{"linkBlock"}, Content: &model.BlockContentOfSmartblock{Smartblock: &model.BlockContentSmartblock{}}}))
smartBlockTest.AddBlock(simple.New(&model.Block{Id: "linkBlock", Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{TargetBlockId: "id1"}}}))
linkObject := smarttest.New("id1")
linkObjectDoc := linkObject.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String("id1"),
bundle.RelationKeyType: domain.String("objectTypeId"),
}))
linkObjectDoc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
})
linkObject.Doc = linkObjectDoc
objectGetter.EXPECT().GetObject(context.Background(), "id").Return(smartBlockTest, nil)
objectGetter.EXPECT().GetObject(context.Background(), "id1").Return(linkObject, nil)
e := &export{
objectStore: storeFixture,
sbtProvider: provider,
picker: objectGetter,
}
expCtx := newExportContext(e, pb.RpcObjectListExportRequest{
@ -396,7 +880,7 @@ func Test_docsForExport(t *testing.T) {
})
// when
err = expCtx.docsForExport()
err := expCtx.docsForExport()
// then
assert.Nil(t, err)
@ -417,14 +901,32 @@ func Test_docsForExport(t *testing.T) {
bundle.RelationKeySpaceId: domain.String(spaceId),
},
})
err := storeFixture.SpaceIndex(spaceId).UpdateObjectLinks(context.Background(), "id", []string{"id1"})
assert.Nil(t, err)
objectGetter := mock_cache.NewMockObjectGetterComponent(t)
smartBlockTest := smarttest.New("id")
doc := smartBlockTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
bundle.RelationKeyId: domain.String("id"),
bundle.RelationKeyType: domain.String("objectTypeId"),
}))
doc.AddRelationLinks(&model.RelationLink{
Key: bundle.RelationKeyId.String(),
Format: model.RelationFormat_longtext,
}, &model.RelationLink{
Key: bundle.RelationKeyType.String(),
Format: model.RelationFormat_longtext,
})
smartBlockTest.Doc = doc
smartBlockTest.AddBlock(simple.New(&model.Block{Id: "id", ChildrenIds: []string{"linkBlock"}, Content: &model.BlockContentOfSmartblock{Smartblock: &model.BlockContentSmartblock{}}}))
smartBlockTest.AddBlock(simple.New(&model.Block{Id: "linkBlock", Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{TargetBlockId: "id1"}}}))
objectGetter.EXPECT().GetObject(context.Background(), "id").Return(smartBlockTest, nil)
provider := mock_typeprovider.NewMockSmartBlockTypeProvider(t)
provider.EXPECT().Type(spaceId, "id1").Return(smartblock.SmartBlockTypePage, nil)
e := &export{
objectStore: storeFixture,
sbtProvider: provider,
picker: objectGetter,
}
expCtx := newExportContext(e, pb.RpcObjectListExportRequest{
SpaceId: spaceId,
@ -433,7 +935,7 @@ func Test_docsForExport(t *testing.T) {
})
// when
err = expCtx.docsForExport()
err := expCtx.docsForExport()
// then
assert.Nil(t, err)
@ -770,10 +1272,10 @@ func Test_docsForExport(t *testing.T) {
linkedObjectId := "linkedObjectId"
storeFixture.AddObjects(t, spaceId, []objectstore.TestObject{
{
bundle.RelationKeyId: domain.String("id"),
domain.RelationKey(relationKey): domain.String("test"),
bundle.RelationKeyType: domain.String(objectTypeKey),
bundle.RelationKeySpaceId: domain.String(spaceId),
bundle.RelationKeyId: domain.String("id"),
relationKey: domain.String("test"),
bundle.RelationKeyType: domain.String(objectTypeKey),
bundle.RelationKeySpaceId: domain.String(spaceId),
},
{
bundle.RelationKeyId: domain.String(relationKey),
@ -810,9 +1312,6 @@ func Test_docsForExport(t *testing.T) {
},
})
err = storeFixture.SpaceIndex(spaceId).UpdateObjectLinks(context.Background(), templateId, []string{linkedObjectId})
assert.Nil(t, err)
objectGetter := mock_cache.NewMockObjectGetter(t)
template := smarttest.New(templateId)
@ -828,6 +1327,8 @@ func Test_docsForExport(t *testing.T) {
Format: model.RelationFormat_longtext,
})
template.Doc = templateDoc
template.AddBlock(simple.New(&model.Block{Id: templateId, ChildrenIds: []string{"linkBlock"}, Content: &model.BlockContentOfSmartblock{Smartblock: &model.BlockContentSmartblock{}}}))
template.AddBlock(simple.New(&model.Block{Id: "linkBlock", Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{TargetBlockId: linkedObjectId}}}))
smartBlockTest := smarttest.New("id")
doc := smartBlockTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{

View file

@ -4,6 +4,8 @@ import (
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/samber/lo"
@ -13,6 +15,7 @@ import (
type Directory struct {
fileReaders map[string]struct{}
importPath string
rootDirs map[string]bool
}
func NewDirectory() *Directory {
@ -23,6 +26,9 @@ func (d *Directory) Initialize(importPath string) error {
files := make(map[string]struct{})
err := filepath.Walk(importPath,
func(path string, info os.FileInfo, err error) error {
if strings.HasPrefix(info.Name(), ".DS_Store") {
return nil
}
if info != nil && !info.IsDir() {
files[path] = struct{}{}
}
@ -31,6 +37,7 @@ func (d *Directory) Initialize(importPath string) error {
)
d.fileReaders = files
d.importPath = importPath
d.rootDirs = findNonEmptyDirs(files)
if err != nil {
return err
}
@ -80,7 +87,44 @@ func (d *Directory) CountFilesWithGivenExtensions(extension []string) int {
}
func (d *Directory) IsRootFile(fileName string) bool {
return filepath.Dir(fileName) == d.importPath
fileDir := filepath.Dir(fileName)
return fileDir == d.importPath || d.rootDirs[fileDir]
}
func (d *Directory) Close() {}
func findNonEmptyDirs(files map[string]struct{}) map[string]bool {
dirs := make([]string, 0, len(files))
for file := range files {
dir := filepath.Dir(file)
if dir == "." {
return map[string]bool{dir: true}
}
dirs = append(dirs, dir)
}
sort.Strings(dirs)
result := make(map[string]bool)
visited := make(map[string]bool)
for _, dir := range dirs {
if _, ok := visited[dir]; ok {
continue
}
visited[dir] = true
if isSubdirectoryOfAny(dir, result) {
continue
}
result[dir] = true
}
return result
}
func isSubdirectoryOfAny(dir string, directories map[string]bool) bool {
for base := range directories {
if strings.HasPrefix(dir, base+string(filepath.Separator)) {
return true
}
}
return false
}

View file

@ -0,0 +1,102 @@
package source
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func createTestDir(tempDir string, files map[string]string) error {
for name, content := range files {
fullPath := filepath.Join(tempDir, name)
err := os.MkdirAll(filepath.Dir(fullPath), 0777)
if err != nil {
return err
}
file, err := os.Create(fullPath)
if err != nil {
return err
}
defer file.Close()
_, err = file.Write([]byte(content))
if err != nil {
return err
}
}
return nil
}
func TestDirectory_Initialize(t *testing.T) {
t.Run("success", func(t *testing.T) {
// given
files := map[string]string{
"file1.txt": "test",
filepath.Join("folder", "file2.txt"): "test",
}
tempDir := t.TempDir()
err := createTestDir(tempDir, files)
defer os.RemoveAll(tempDir)
assert.NoError(t, err)
// when
directory := NewDirectory()
err = directory.Initialize(tempDir)
// then
assert.NoError(t, err)
assert.Equal(t, tempDir, directory.importPath)
assert.Len(t, directory.fileReaders, 2)
expectedRoots := map[string]bool{tempDir: true}
assert.Equal(t, expectedRoots, directory.rootDirs)
})
t.Run("directory with another dir inside", func(t *testing.T) {
// given
files := map[string]string{
filepath.Join("folder", "file2.txt"): "test",
filepath.Join("folder", "file3.txt"): "test",
filepath.Join("folder", "folder1", "file4.txt"): "test",
}
tempDir := t.TempDir()
err := createTestDir(tempDir, files)
assert.NoError(t, err)
defer os.RemoveAll(tempDir)
// when
directory := NewDirectory()
err = directory.Initialize(tempDir)
// then
assert.NoError(t, err)
assert.Equal(t, tempDir, directory.importPath)
assert.Len(t, directory.fileReaders, 3)
expectedRoots := map[string]bool{filepath.Join(tempDir, "folder"): true}
assert.Equal(t, expectedRoots, directory.rootDirs)
})
t.Run("directory with 2 dirs inside", func(t *testing.T) {
// given
files := map[string]string{
filepath.Join("folder", "file2.txt"): "test",
filepath.Join("folder1", "folder2", "file4.txt"): "test",
}
tempDir := t.TempDir()
err := createTestDir(tempDir, files)
assert.NoError(t, err)
defer os.RemoveAll(tempDir)
// when
directory := NewDirectory()
err = directory.Initialize(tempDir)
// then
assert.NoError(t, err)
assert.Equal(t, tempDir, directory.importPath)
assert.Len(t, directory.fileReaders, 2)
expectedRoots := map[string]bool{filepath.Join(tempDir, "folder"): true, filepath.Join(tempDir, "folder1", "folder2"): true}
assert.Equal(t, expectedRoots, directory.rootDirs)
})
}

View file

@ -20,6 +20,7 @@ type Zip struct {
archiveReader *zip.ReadCloser
fileReaders map[string]*zip.File
originalToNormalizedNames map[string]string
rootDirs map[string]bool
}
func NewZip() *Zip {
@ -33,16 +34,20 @@ func (z *Zip) Initialize(importPath string) error {
return err
}
fileReaders := make(map[string]*zip.File, len(archiveReader.File))
filePaths := make(map[string]struct{}, len(archiveReader.File))
for i, f := range archiveReader.File {
if strings.HasPrefix(f.Name, "__MACOSX/") {
continue
}
normalizedName := normalizeName(f, i)
fileReaders[normalizedName] = f
filePaths[normalizedName] = struct{}{}
if normalizedName != f.Name {
z.originalToNormalizedNames[f.Name] = normalizedName
}
}
z.rootDirs = findNonEmptyDirs(filePaths)
z.fileReaders = fileReaders
return nil
}
@ -101,7 +106,8 @@ func (z *Zip) Close() {
}
func (z *Zip) IsRootFile(fileName string) bool {
return filepath.Dir(fileName) == "."
fileDir := filepath.Dir(fileName)
return fileDir == "." || z.rootDirs[fileDir]
}
func (z *Zip) GetFileOriginalName(fileName string) string {

View file

@ -0,0 +1,119 @@
package source
import (
"archive/zip"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func createTestZip(t *testing.T, files map[string]string) (string, error) {
tmpFile, err := ioutil.TempFile(t.TempDir(), "test-*.zip")
if err != nil {
return "", err
}
defer tmpFile.Close()
zipWriter := zip.NewWriter(tmpFile)
for name, content := range files {
writer, err := zipWriter.Create(name)
if err != nil {
return "", err
}
_, err = writer.Write([]byte(content))
if err != nil {
return "", err
}
}
if err := zipWriter.Close(); err != nil {
return "", err
}
return tmpFile.Name(), nil
}
func TestZip_Initialize(t *testing.T) {
t.Run("success", func(t *testing.T) {
// given
files := map[string]string{
"file1.txt": "test",
filepath.Join("folder", "file2.txt"): "test",
}
zipPath, err := createTestZip(t, files)
assert.NoError(t, err)
defer os.Remove(zipPath)
// when
zipInstance := NewZip()
err = zipInstance.Initialize(zipPath)
// then
assert.NoError(t, err)
assert.NotNil(t, zipInstance.archiveReader)
assert.Len(t, zipInstance.fileReaders, 2)
expectedRoots := map[string]bool{".": true}
assert.Equal(t, expectedRoots, zipInstance.rootDirs)
})
t.Run("zip files with dir inside", func(t *testing.T) {
// given
files := map[string]string{
filepath.Join("folder", "file2.txt"): "test",
filepath.Join("folder", "file3.txt"): "test",
filepath.Join("folder", "folder1", "file4.txt"): "test",
}
zipPath, err := createTestZip(t, files)
assert.NoError(t, err)
defer os.Remove(zipPath)
// when
zipInstance := NewZip()
err = zipInstance.Initialize(zipPath)
// then
assert.NoError(t, err)
assert.NotNil(t, zipInstance.archiveReader)
assert.Len(t, zipInstance.fileReaders, 3)
expectedRoots := map[string]bool{"folder": true}
assert.Equal(t, expectedRoots, zipInstance.rootDirs)
})
t.Run("zip files with 2 dirs inside", func(t *testing.T) {
// given
files := map[string]string{
filepath.Join("folder", "file2.txt"): "test",
filepath.Join("folder1", "folder2", "file4.txt"): "test",
}
zipPath, err := createTestZip(t, files)
assert.NoError(t, err)
defer os.Remove(zipPath)
// when
zipInstance := NewZip()
err = zipInstance.Initialize(zipPath)
// then
assert.NoError(t, err)
assert.NotNil(t, zipInstance.archiveReader)
assert.Len(t, zipInstance.fileReaders, 2)
expectedRoots := map[string]bool{"folder": true, filepath.Join("folder1", "folder2"): true}
assert.Equal(t, expectedRoots, zipInstance.rootDirs)
})
t.Run("invalid path", func(t *testing.T) {
// given
zipInstance := NewZip()
// when
err := zipInstance.Initialize("invalid_path.zip")
// then
assert.Error(t, err)
})
}

View file

@ -285,7 +285,7 @@ func (p *Pb) getSnapshotFromFile(rd io.ReadCloser, name string) (*common.Snapsho
defer rd.Close()
if filepath.Ext(name) == ".json" {
snapshot := &pb.SnapshotWithType{}
um := jsonpb.Unmarshaler{}
um := jsonpb.Unmarshaler{AllowUnknownFields: true}
if uErr := um.Unmarshal(rd, snapshot); uErr != nil {
return nil, fmt.Errorf("PB:GetSnapshot %w", uErr)
}

View file

@ -4,9 +4,8 @@ import (
"sync"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/commonspace/spacestorage"
"github.com/anyproto/anytype-heart/space/spacecore/storage"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
)
const CName = "block.object.resolver"
@ -21,12 +20,12 @@ func New() Resolver {
}
type resolver struct {
storage storage.ClientStorage
objectStore objectstore.ObjectStore
sync.Mutex
}
func (r *resolver) Init(a *app.App) (err error) {
r.storage = a.MustComponent(spacestorage.CName).(storage.ClientStorage)
r.objectStore = a.MustComponent(objectstore.CName).(objectstore.ObjectStore)
return
}
@ -35,5 +34,5 @@ func (r *resolver) Name() (name string) {
}
func (r *resolver) ResolveSpaceID(objectID string) (string, error) {
return r.storage.GetSpaceID(objectID)
return r.objectStore.GetSpaceId(objectID)
}

View file

@ -52,6 +52,7 @@ type Cache interface {
GetObjectWithTimeout(ctx context.Context, id string) (sb smartblock.SmartBlock, err error)
DoLockedIfNotExists(objectID string, proc func() error) error
Remove(ctx context.Context, objectID string) error
TryRemove(objectId string) (bool, error)
CloseBlocks()
Close(ctx context.Context) error
@ -155,6 +156,10 @@ func (c *objectCache) Remove(ctx context.Context, objectID string) error {
return err
}
func (c *objectCache) TryRemove(objectId string) (bool, error) {
return c.cache.TryRemove(objectId)
}
func (c *objectCache) GetObjectWithTimeout(ctx context.Context, id string) (sb smartblock.SmartBlock, err error) {
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc

View file

@ -785,6 +785,62 @@ func (_c *MockCache_Remove_Call) RunAndReturn(run func(context.Context, string)
return _c
}
// TryRemove provides a mock function with given fields: objectId
func (_m *MockCache) TryRemove(objectId string) (bool, error) {
ret := _m.Called(objectId)
if len(ret) == 0 {
panic("no return value specified for TryRemove")
}
var r0 bool
var r1 error
if rf, ok := ret.Get(0).(func(string) (bool, error)); ok {
return rf(objectId)
}
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(objectId)
} else {
r0 = ret.Get(0).(bool)
}
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(objectId)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockCache_TryRemove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TryRemove'
type MockCache_TryRemove_Call struct {
*mock.Call
}
// TryRemove is a helper method to define mock.On call
// - objectId string
func (_e *MockCache_Expecter) TryRemove(objectId interface{}) *MockCache_TryRemove_Call {
return &MockCache_TryRemove_Call{Call: _e.mock.On("TryRemove", objectId)}
}
func (_c *MockCache_TryRemove_Call) Run(run func(objectId string)) *MockCache_TryRemove_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockCache_TryRemove_Call) Return(_a0 bool, _a1 error) *MockCache_TryRemove_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockCache_TryRemove_Call) RunAndReturn(run func(string) (bool, error)) *MockCache_TryRemove_Call {
_c.Call.Return(run)
return _c
}
// NewMockCache creates a new instance of MockCache. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockCache(t interface {

View file

@ -20,7 +20,7 @@ func Test_Payloads(t *testing.T) {
changePayload := []byte("some")
keys, err := accountdata.NewRandom()
require.NoError(t, err)
aclList, err := list.NewTestDerivedAcl("spaceId", keys)
aclList, err := list.NewInMemoryDerivedAcl("spaceId", keys)
require.NoError(t, err)
timestamp := time.Now().Add(time.Hour).Unix()

View file

@ -8,7 +8,6 @@ import (
"github.com/anyproto/any-sync/app"
"github.com/pkg/errors"
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/restriction"
"github.com/anyproto/anytype-heart/core/block/source"
@ -65,7 +64,6 @@ type service struct {
bookmarkService bookmarkService
spaceService space.Service
templateService templateService
lastUsedUpdater lastused.ObjectUsageUpdater
archiver objectArchiver
}
@ -79,7 +77,6 @@ func (s *service) Init(a *app.App) (err error) {
s.collectionService = app.MustComponent[collectionService](a)
s.spaceService = app.MustComponent[space.Service](a)
s.templateService = app.MustComponent[templateService](a)
s.lastUsedUpdater = app.MustComponent[lastused.ObjectUsageUpdater](a)
s.archiver = app.MustComponent[objectArchiver](a)
return nil
}
@ -211,3 +208,9 @@ func buildDateObject(space clientspace.Space, details *domain.Details) (string,
details, err = detailsGetter.DetailsFromId()
return dateObject.Id(), details, err
}
func setOriginalCreatedTimestamp(state *state.State, details *domain.Details) {
if createDate := details.GetInt64(bundle.RelationKeyCreatedDate); createDate != 0 {
state.SetOriginalCreatedTimestamp(createDate)
}
}

View file

@ -9,7 +9,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/anyproto/anytype-heart/core/block/editor/lastused/mock_lastused"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/domain"
@ -26,7 +25,6 @@ type fixture struct {
spaceService *mock_space.MockService
spc *mock_clientspace.MockSpace
templateService *testTemplateService
lastUsedService *mock_lastused.MockObjectUsageUpdater
service Service
}
@ -35,19 +33,16 @@ func newFixture(t *testing.T) *fixture {
spc := mock_clientspace.NewMockSpace(t)
templateSvc := &testTemplateService{}
lastUsedSvc := mock_lastused.NewMockObjectUsageUpdater(t)
s := &service{
spaceService: spaceService,
templateService: templateSvc,
lastUsedUpdater: lastUsedSvc,
}
return &fixture{
spaceService: spaceService,
spc: spc,
templateService: templateSvc,
lastUsedService: lastUsedSvc,
service: s,
}
}
@ -77,7 +72,6 @@ func TestService_CreateObject(t *testing.T) {
f.spaceService.EXPECT().Get(mock.Anything, mock.Anything).Return(f.spc, nil)
f.spc.EXPECT().CreateTreeObject(mock.Anything, mock.Anything).Return(sb, nil)
f.spc.EXPECT().Id().Return(spaceId)
f.lastUsedService.EXPECT().UpdateLastUsedDate(spaceId, bundle.TypeKeyTemplate, mock.Anything).Return()
// when
id, _, err := f.service.CreateObject(context.Background(), spaceId, CreateObjectRequest{

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"strings"
"time"
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
"go.uber.org/zap"
@ -213,6 +214,7 @@ func (s *service) reinstallObject(
st.SetDetails(installingDetails)
st.SetDetailAndBundledRelation(bundle.RelationKeyIsUninstalled, domain.Bool(false))
st.SetDetailAndBundledRelation(bundle.RelationKeyIsDeleted, domain.Bool(false))
st.SetOriginalCreatedTimestamp(time.Now().Unix())
key = domain.TypeKey(st.UniqueKeyInternal())
details = st.CombinedDetails()
@ -253,9 +255,7 @@ func (s *service) prepareDetailsForInstallingObject(
details.SetString(bundle.RelationKeySpaceId, spaceID)
details.SetString(bundle.RelationKeySourceObject, sourceId)
details.SetBool(bundle.RelationKeyIsReadonly, false)
// we should delete old createdDate as it belongs to source object from marketplace
details.Delete(bundle.RelationKeyCreatedDate)
details.SetInt64(bundle.RelationKeyCreatedDate, time.Now().Unix())
if isNewSpace {
lastused.SetLastUsedDateForInitialObjectType(sourceId, details)

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