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

GO-3504 Merge main 2

This commit is contained in:
kirillston 2024-08-08 16:34:13 +03:00
commit 4d554f0433
No known key found for this signature in database
GPG key ID: 88218A7F1109754B
278 changed files with 25273 additions and 10725 deletions

22
.github/install_macos_sdk.sh vendored Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Install an older MacOS SDK
OSX_SDK_DIR="$(xcode-select -p)/Platforms/MacOSX.platform/Developer/SDKs"
export MACOSX_DEPLOYMENT_TARGET=$1
export MACOSX_SDK_VERSION=$MACOSX_DEPLOYMENT_TARGET
export OSX_SYSROOT="${OSX_SDK_DIR}/MacOSX${MACOSX_SDK_VERSION}.sdk"
FILENAME="MacOSX${MACOSX_SDK_VERSION}.sdk.tar.xz"
DOWNLOAD_URL="https://github.com/phracker/MacOSX-SDKs/releases/download/10.15/${FILENAME}"
if [[ ! -d ${OSX_SYSROOT}} ]]; then
echo "MacOS SDK ${MACOSX_SDK_VERSION} is missing, downloading..."
curl -L -O --connect-timeout 5 --max-time 10 --retry 10 --retry-delay 0 --retry-max-time 40 --retry-connrefused --retry-all-errors \
${DOWNLOAD_URL}
tar -xf ${FILENAME} -C "$(dirname ${OSX_SYSROOT})"
fi
plutil -replace MinimumSDKVersion -string ${MACOSX_SDK_VERSION} $(xcode-select -p)/Platforms/MacOSX.platform/Info.plist
plutil -replace DTSDKName -string macosx${MACOSX_SDK_VERSION}internal $(xcode-select -p)/Platforms/MacOSX.platform/Info.plist
echo "SDKROOT=${OSX_SYSROOT}" >> ${GITHUB_ENV}

View file

@ -10,7 +10,7 @@ on:
run-on-runner:
description: 'Specify the runner to use'
required: true
default: 'self-hosted'
default: 'ARM64'
perf-test:
description: 'Run perf test times'
required: true
@ -32,11 +32,11 @@ permissions:
name: Build
jobs:
build:
runs-on: ${{ github.event_name == 'push' && 'macos-11' || (github.event.inputs.run-on-runner || 'self-hosted') }}
runs-on: ${{ github.event_name == 'push' && 'macos-12' || (github.event.inputs.run-on-runner || 'ARM64') }}
steps:
- name: validate agent
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.run-on-runner }}" != "self-hosted" ]]; then
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.run-on-runner }}" != "ARM64" ]]; then
echo "Invalid runner"
exit 1
fi
@ -44,7 +44,7 @@ jobs:
uses: actions/setup-go@v1
with:
go-version: 1.22
if: github.event.inputs.run-on-runner != 'self-hosted' && github.event_name != 'schedule'
if: github.event.inputs.run-on-runner != 'ARM64' && github.event_name != 'schedule'
- name: Setup GO
run: |
go version
@ -60,16 +60,17 @@ jobs:
git fetch
git checkout db6184738b77fbd5089e5fa1112177f391c91b24
go install github.com/mitchellh/gox
if: github.event.inputs.run-on-runner != 'self-hosted' && github.event_name != 'schedule'
if: github.event.inputs.run-on-runner != 'ARM64' && github.event_name != 'schedule'
- 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
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
npm i -g node-gyp
if: github.event.inputs.run-on-runner != 'self-hosted' && github.event_name != 'schedule'
if: github.event.inputs.run-on-runner != 'ARM64' && github.event_name != 'schedule'
- name: Checkout
uses: actions/checkout@v3
- uses: actions/cache@v3
@ -79,6 +80,9 @@ jobs:
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.go-version }}-
- name: Install old MacOS SDK (for backward compatibility of CGO)
run: source .github/install_macos_sdk.sh 10.15
if: github.event.inputs.run-on-runner != 'ARM64'
- name: Set env vars
env:
UNSPLASH_KEY: ${{ secrets.UNSPLASH_KEY }}
@ -97,7 +101,6 @@ jobs:
fi
echo VERSION=${VERSION} >> $GITHUB_ENV
echo MAVEN_ARTIFACT_VERSION=${VERSION} >> $GITHUB_ENV
echo SDKROOT=$(xcrun --sdk macosx --show-sdk-path) >> $GITHUB_ENV
echo GOPRIVATE=github.com/anyproto >> $GITHUB_ENV
echo $(pwd)/deps >> $GITHUB_PATH
echo "${GOBIN}" >> $GITHUB_PATH
@ -115,9 +118,14 @@ jobs:
which gomobile
- name: Cross-compile library mac/win
run: |
make download-tantivy-all
echo $FLAGS
mkdir -p .release
gox -cgo -ldflags="$FLAGS" -osarch="darwin/amd64 darwin/arm64" --tags="envproduction nographviz nowatchdog nosigar nomutexdeadlockdetector" -output="{{.OS}}-{{.Arch}}" github.com/anyproto/anytype-heart/cmd/grpcserver
echo $SDKROOT
gox -cgo -ldflags="$FLAGS" -osarch="darwin/amd64" --tags="envproduction nographviz nowatchdog nosigar nomutexdeadlockdetector" -output="{{.OS}}-{{.Arch}}" github.com/anyproto/anytype-heart/cmd/grpcserver
export SDKROOT=$(xcrun --sdk macosx --show-sdk-path)
echo $SDKROOT
gox -cgo -ldflags="$FLAGS" -osarch="darwin/arm64" --tags="envproduction nographviz nowatchdog nosigar nomutexdeadlockdetector" -output="{{.OS}}-{{.Arch}}" github.com/anyproto/anytype-heart/cmd/grpcserver
make protos-server
CC="x86_64-w64-mingw32-gcc" CXX="x86_64-w64-mingw32-g++" gox -cgo -ldflags="$FLAGS -linkmode external -extldflags=-static" -osarch="windows/amd64" --tags="envproduction nographviz nowatchdog nosigar nomutexdeadlockdetector noheic" -output="{{.OS}}-{{.Arch}}" github.com/anyproto/anytype-heart/cmd/grpcserver
ls -lha .
@ -125,12 +133,13 @@ jobs:
- name: run perf tests
run: |
echo "Running perf tests"
make download-tantivy-all
RUN_COUNT=${{ github.event.inputs.perf-test }}
if [[ "${{ github.event_name }}" == "schedule" ]]; then
RUN_COUNT=10
fi
cd cmd/perftester/
go run main.go $RUN_COUNT
CGO_ENABLED="1" go run main.go $RUN_COUNT
env:
ANYTYPE_REPORT_MEMORY: 'true'
TEST_MNEMONIC: ${{ secrets.TEST_MNEMONIC }}
@ -189,7 +198,7 @@ jobs:
mv js_${VERSION}_${OSARCH}.zip .release/
done
if: github.event_name == 'push'
- name: Pack server unix
- name: Pack server osx
run: |
declare -a arr=("darwin-amd64" "darwin-arm64")
for i in "${arr[@]}"
@ -314,10 +323,8 @@ jobs:
run: |
sudo apt update
sudo apt install -y protobuf-compiler libprotoc-dev
curl -O https://musl.cc/aarch64-linux-musl-cross.tgz
curl -O https://musl.cc/x86_64-linux-musl-native.tgz
tar xzf aarch64-linux-musl-cross.tgz -C $HOME
tar xzf x86_64-linux-musl-native.tgz -C $HOME
curl -O https://pub-c60a000d68b544109df4fe5837762101.r2.dev/linux-compiler-musl-x86.zip
unzip linux-compiler-musl-x86.zip -d $HOME
npm i -g node-gyp
- name: Checkout
uses: actions/checkout@v3
@ -349,10 +356,10 @@ jobs:
make setup-go
- name: Cross-compile library for linux amd64/arm64
run: |
make download-tantivy-all
echo $FLAGS
mkdir -p .release
CXX=$HOME/x86_64-linux-musl-native/bin/x86_64-linux-musl-g++ CC=$HOME/x86_64-linux-musl-native/bin/x86_64-linux-musl-gcc gox -cgo -osarch="linux/amd64" -ldflags="$FLAGS -linkmode external -extldflags=-static" --tags="envproduction nographviz nowatchdog nosigar nomutexdeadlockdetector" -output="{{.OS}}-{{.Arch}}" github.com/anyproto/anytype-heart/cmd/grpcserver
CXX=$HOME/aarch64-linux-musl-cross/bin/aarch64-linux-musl-g++ CC=$HOME/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc gox -cgo -osarch="linux/arm64" -ldflags="$FLAGS -linkmode external -extldflags=-static" --tags="envproduction nographviz nowatchdog nosigar nomutexdeadlockdetector" -output="{{.OS}}-{{.Arch}}" github.com/anyproto/anytype-heart/cmd/grpcserver
CXX=$HOME/linux-compiler-musl-x86/bin/x86_64-linux-musl-g++ CC=$HOME/linux-compiler-musl-x86/bin/x86_64-linux-musl-gcc gox -cgo -osarch="linux/amd64" -ldflags="$FLAGS -linkmode external -extldflags=-static" --tags="envproduction nographviz nowatchdog nosigar nomutexdeadlockdetector" -output="{{.OS}}-{{.Arch}}" github.com/anyproto/anytype-heart/cmd/grpcserver
make protos-server
- name: Make JS protos
run: |
@ -379,7 +386,7 @@ jobs:
retention-days: 1
- name: Pack server unix
run: |
declare -a arr=("linux-amd64" "linux-arm64")
declare -a arr=("linux-amd64")
for i in "${arr[@]}"
do
OSARCH=${i%.*}

View file

@ -1,5 +1,10 @@
on: [ pull_request ]
name: Test
on:
push:
branches:
- main
pull_request:
branches:
- '*'
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
@ -47,6 +52,7 @@ jobs:
license_finder --enabled-package-managers gomodules
- name: Generate mocks
run: |
make download-tantivy-all
go install go.uber.org/mock/mockgen@v0.3.0
CGO_ENABLED=1 CGO_CFLAGS="-Wno-deprecated-declarations -Wno-deprecated-non-prototype -Wno-xor-used-as-pow" go generate ./...
- name: Go test
@ -60,7 +66,12 @@ jobs:
PACKAGE_NAMES=$(go list -tags nogrpcserver ./... | grep -v "github.com/anyproto/anytype-heart/cmd/grpserver" | grep -v "github.com/anyproto/anytype-heart/clientlibrary/clib")
rm -rf ~/gotestsum-report
mkdir ~/gotestsum-report
CGO_CFLAGS="-Wno-deprecated-non-prototype -Wno-unknown-warning-option -Wno-deprecated-declarations -Wno-xor-used-as-pow -Wno-single-bit-bitfield-constant-conversion" gotestsum --junitfile ~/gotestsum-report/gotestsum-report.xml -- -tags "nogrpcserver nographviz" -ldflags="-extldflags=-Wl,-ld_classic" -p 1 $(echo $PACKAGE_NAMES) -race -coverprofile=coverage.out -covermode=atomic ./...
if [[ "$GITHUB_REF" == "refs/heads/main" && "$GITHUB_EVENT_NAME" == "push" ]]; then
export RACE=-race
else
echo "run without race detector"
fi
CGO_CFLAGS="-Wno-deprecated-non-prototype -Wno-unknown-warning-option -Wno-deprecated-declarations -Wno-xor-used-as-pow -Wno-single-bit-bitfield-constant-conversion" gotestsum --junitfile ~/gotestsum-report/gotestsum-report.xml -- -tags "nogrpcserver nographviz" -ldflags="-extldflags=-Wl,-ld_classic" -p 1 $(echo $PACKAGE_NAMES) $(echo $RACE) -coverprofile=coverage.out -covermode=atomic ./...
generated_pattern='^\/\/ Code generated .* DO NOT EDIT\.$'
files_list=$(grep -rl "$generated_pattern" . | grep '\.go$' | sed 's/^\.\///')

View file

@ -10,9 +10,12 @@ issues:
exclude-generated: disable
exclude-dirs:
- pkg/lib/pb
- pb
exclude-files:
- '.*_test.go'
- 'mock*'
- 'testMock/*'
- 'clientlibrary/service/service.pb.go'
linters-settings:
unused:

View file

@ -62,7 +62,7 @@ packages:
RpcStore:
github.com/anyproto/anytype-heart/core/block/import/common/objectid:
interfaces:
IdGetterProvider:
IdAndKeyProvider:
github.com/anyproto/anytype-heart/core/block/import/common/objectcreator:
interfaces:
Service:
@ -175,10 +175,26 @@ packages:
github.com/anyproto/anytype-heart/util/linkpreview:
interfaces:
LinkPreview:
github.com/anyproto/anytype-heart/space/spacecore/clientserver:
interfaces:
ClientServer:
github.com/anyproto/anytype-heart/core/peerstatus:
interfaces:
LocalDiscoveryHook:
github.com/anyproto/anytype-heart/space/spacecore/localdiscovery:
interfaces:
Notifier:
config:
dir: "{{.InterfaceDir}}"
outpkg: "{{.PackageName}}"
inpackage: true
github.com/anyproto/anytype-heart/core/block/object/treesyncer:
interfaces:
PeerStatusChecker:
SyncDetailsUpdater:
github.com/anyproto/anytype-heart/core/syncstatus/nodestatus:
interfaces:
NodeStatus:
github.com/anyproto/anytype-heart/core/syncstatus/objectsyncstatus:
interfaces:
Updater:
@ -190,10 +206,13 @@ packages:
github.com/anyproto/anytype-heart/space/spacecore/peermanager:
interfaces:
Updater:
PeerToPeerStatus:
github.com/anyproto/anytype-heart/core/syncstatus/detailsupdater:
interfaces:
SpaceStatusUpdater:
github.com/anyproto/anytype-heart/core/syncstatus/spacesyncstatus:
interfaces:
SpaceIdGetter:
NodeUsage:
NetworkConfig:
Updater:

View file

@ -2,6 +2,7 @@ CUSTOM_NETWORK_FILE ?= ./core/anytype/config/nodes/custom.yml
CLIENT_DESKTOP_PATH ?= ../anytype-ts
CLIENT_ANDROID_PATH ?= ../anytype-kotlin
CLIENT_IOS_PATH ?= ../anytype-swift
TANTIVY_GO_PATH ?= ../tantivy-go
BUILD_FLAGS ?=
export GOLANGCI_LINT_VERSION=1.58.1
@ -66,13 +67,17 @@ test:
@echo 'Running tests...'
@ANYTYPE_LOG_NOGELF=1 go test -cover github.com/anyproto/anytype-heart/...
test-no-cache:
@echo 'Running tests...'
@ANYTYPE_LOG_NOGELF=1 go test -count=1 github.com/anyproto/anytype-heart/...
test-integration:
@echo 'Running integration tests...'
@go test -run=TestBasic -tags=integration -v -count 1 ./tests
test-race:
@echo 'Running tests with race-detector...'
@ANYTYPE_LOG_NOGELF=1 go test -race github.com/anyproto/anytype-heart/...
@ANYTYPE_LOG_NOGELF=1 go test -count=1 -race github.com/anyproto/anytype-heart/...
test-deps:
@echo 'Generating test mocks...'
@ -328,3 +333,53 @@ ifdef GOLANGCI_LINT_BRANCH
else
@golangci-lint run -v ./... --new-from-rev=origin/main --timeout 15m --fix
endif
### Tantivy Section
REPO := anyproto/tantivy-go
VERSION := v0.0.7
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 \
linux-amd64-musl.tar.gz \
windows-amd64.tar.gz
define download_tantivy_lib
curl -L -o $(OUTPUT_DIR)/$(1) https://github.com/$(REPO)/releases/download/$(VERSION)/$(1)
endef
define remove_arch
rm -f $(OUTPUT_DIR)/$(1)
endef
download-tantivy: $(TANTIVY_LIBS)
$(TANTIVY_LIBS):
@mkdir -p $(OUTPUT_DIR)/$(shell echo $@ | cut -d'.' -f1)
$(call download_tantivy_lib,$@)
@tar -C $(OUTPUT_DIR)/$(shell echo $@ | cut -d'.' -f1) -xvzf $(OUTPUT_DIR)/$@
download-tantivy-all-force: download-tantivy
@rm -f $(SHA_FILE)
@for file in $(TANTIVY_LIBS); do \
echo "SHA256 $(OUTPUT_DIR)/$$file" ; \
shasum -a 256 $(OUTPUT_DIR)/$$file | awk '{print $$1 " " "'$(OUTPUT_DIR)/$$file'" }' >> $(SHA_FILE); \
done
@echo "SHA256 checksums generated."
download-tantivy-all: download-tantivy
@echo "Validating SHA256 checksums..."
@shasum -a 256 -c $(SHA_FILE) --status || { echo "Hash mismatch detected."; exit 1; }
@echo "All files are valid."
download-tantivy-local:
@mkdir -p $(OUTPUT_DIR)
@cp -r $(TANTIVY_GO_PATH)/libs/ $(OUTPUT_DIR)

View file

@ -25,311 +25,314 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
func init() { proto.RegisterFile("pb/protos/service/service.proto", fileDescriptor_93a29dc403579097) }
var fileDescriptor_93a29dc403579097 = []byte{
// 4850 bytes of a gzipped FileDescriptorProto
// 4904 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x9d, 0xdd, 0x6f, 0x24, 0x49,
0x52, 0xc0, 0xb7, 0x5f, 0x58, 0xa8, 0xe3, 0x16, 0xe8, 0x85, 0x65, 0x6f, 0xb9, 0x9b, 0x99, 0x9d,
0x9d, 0x0f, 0xcf, 0x78, 0xdc, 0xf6, 0xce, 0xec, 0xc7, 0xb1, 0x87, 0x84, 0x3c, 0xf6, 0xd8, 0x6b,
0xce, 0xf6, 0x18, 0x77, 0x7b, 0x46, 0x5a, 0x09, 0x89, 0x72, 0x75, 0xb8, 0x5d, 0xb8, 0xba, 0xb2,
0x2e, 0x2b, 0xbb, 0x3d, 0x7d, 0x08, 0x04, 0x02, 0x81, 0x40, 0x20, 0x4e, 0x7c, 0xbd, 0xf0, 0x80,
0x84, 0xf8, 0x63, 0x78, 0xbc, 0x27, 0xc4, 0x23, 0xda, 0xfd, 0x47, 0x4e, 0x95, 0x95, 0x95, 0x1f,
0x51, 0x19, 0x59, 0xe5, 0x7d, 0x9a, 0x51, 0xc7, 0x2f, 0x22, 0x32, 0x2b, 0x23, 0x33, 0x23, 0x3f,
0xaa, 0x1c, 0xdd, 0x2e, 0xce, 0x37, 0x0b, 0xce, 0x04, 0x2b, 0x37, 0x4b, 0xe0, 0xcb, 0x34, 0x81,
0xe6, 0xdf, 0x91, 0xfc, 0x79, 0xf8, 0x76, 0x9c, 0xaf, 0xc4, 0xaa, 0x80, 0x0f, 0xde, 0x37, 0x64,
0xc2, 0xe6, 0xf3, 0x38, 0x9f, 0x96, 0x35, 0xf2, 0xc1, 0x7b, 0x46, 0x02, 0x4b, 0xc8, 0x85, 0xfa,
0xfd, 0xe9, 0x7f, 0xff, 0xef, 0x20, 0x7a, 0x67, 0x27, 0x4b, 0x21, 0x17, 0x3b, 0x4a, 0x63, 0xf8,
0x55, 0xf4, 0xdd, 0xed, 0xa2, 0xd8, 0x07, 0xf1, 0x0a, 0x78, 0x99, 0xb2, 0x7c, 0xf8, 0xd1, 0x48,
0x39, 0x18, 0x9d, 0x16, 0xc9, 0x68, 0xbb, 0x28, 0x46, 0x46, 0x38, 0x3a, 0x85, 0x9f, 0x2c, 0xa0,
0x14, 0x1f, 0xdc, 0x0b, 0x43, 0x65, 0xc1, 0xf2, 0x12, 0x86, 0x17, 0xd1, 0x6f, 0x6c, 0x17, 0xc5,
0x18, 0xc4, 0x2e, 0x54, 0x15, 0x18, 0x8b, 0x58, 0xc0, 0xf0, 0x61, 0x4b, 0xd5, 0x05, 0xb4, 0x8f,
0xb5, 0x6e, 0x50, 0xf9, 0x99, 0x44, 0xdf, 0xa9, 0xfc, 0x5c, 0x2e, 0xc4, 0x94, 0x5d, 0xe7, 0xc3,
0x0f, 0xdb, 0x8a, 0x4a, 0xa4, 0x6d, 0xdf, 0x0d, 0x21, 0xca, 0xea, 0xeb, 0xe8, 0x57, 0x5f, 0xc7,
0x59, 0x06, 0x62, 0x87, 0x43, 0x55, 0x70, 0x57, 0xa7, 0x16, 0x8d, 0x6a, 0x99, 0xb6, 0xfb, 0x51,
0x90, 0x51, 0x86, 0xbf, 0x8a, 0xbe, 0x5b, 0x4b, 0x4e, 0x21, 0x61, 0x4b, 0xe0, 0x43, 0xaf, 0x96,
0x12, 0x12, 0x8f, 0xbc, 0x05, 0x61, 0xdb, 0x3b, 0x2c, 0x5f, 0x02, 0x17, 0x7e, 0xdb, 0x4a, 0x18,
0xb6, 0x6d, 0x20, 0x65, 0xfb, 0xef, 0x06, 0xd1, 0xf7, 0xb7, 0x93, 0x84, 0x2d, 0x72, 0x71, 0xc8,
0x92, 0x38, 0x3b, 0x4c, 0xf3, 0xab, 0x63, 0xb8, 0xde, 0xb9, 0xac, 0xf8, 0x7c, 0x06, 0xc3, 0x67,
0xee, 0x53, 0xad, 0xd1, 0x91, 0x66, 0x47, 0x36, 0xac, 0x7d, 0x7f, 0x72, 0x33, 0x25, 0x55, 0x96,
0x7f, 0x1a, 0x44, 0xb7, 0x70, 0x59, 0xc6, 0x2c, 0x5b, 0x82, 0x29, 0xcd, 0xa7, 0x1d, 0x86, 0x5d,
0x5c, 0x97, 0xe7, 0xb3, 0x9b, 0xaa, 0xa9, 0x12, 0x65, 0xd1, 0xbb, 0x76, 0xb8, 0x8c, 0xa1, 0x94,
0xdd, 0xe9, 0x11, 0x1d, 0x11, 0x0a, 0xd1, 0x9e, 0x1f, 0xf7, 0x41, 0x95, 0xb7, 0x34, 0x1a, 0x2a,
0x6f, 0x19, 0x2b, 0xb5, 0xb3, 0x35, 0xaf, 0x05, 0x8b, 0xd0, 0xbe, 0x1e, 0xf5, 0x20, 0x95, 0xab,
0x3f, 0x8e, 0x7e, 0xed, 0x35, 0xe3, 0x57, 0x65, 0x11, 0x27, 0xa0, 0xba, 0xc2, 0x7d, 0x57, 0xbb,
0x91, 0xe2, 0xde, 0xf0, 0xa0, 0x0b, 0xb3, 0x82, 0xb6, 0x11, 0xbe, 0x2c, 0x00, 0x8f, 0x41, 0x46,
0xb1, 0x12, 0x52, 0x41, 0x8b, 0x21, 0x65, 0xfb, 0x2a, 0x1a, 0x1a, 0xdb, 0xe7, 0x7f, 0x02, 0x89,
0xd8, 0x9e, 0x4e, 0x71, 0xab, 0x18, 0x5d, 0x49, 0x8c, 0xb6, 0xa7, 0x53, 0xaa, 0x55, 0xfc, 0xa8,
0x72, 0x76, 0x1d, 0xbd, 0x87, 0x9c, 0x1d, 0xa6, 0xa5, 0x74, 0xb8, 0x11, 0xb6, 0xa2, 0x30, 0xed,
0x74, 0xd4, 0x17, 0x57, 0x8e, 0xff, 0x62, 0x10, 0x7d, 0xcf, 0xe3, 0xf9, 0x14, 0xe6, 0x6c, 0x09,
0xc3, 0xad, 0x6e, 0x6b, 0x35, 0xa9, 0xfd, 0x7f, 0x7c, 0x03, 0x0d, 0x4f, 0x98, 0x8c, 0x21, 0x83,
0x44, 0x90, 0x61, 0x52, 0x8b, 0x3b, 0xc3, 0x44, 0x63, 0x56, 0x0f, 0x6b, 0x84, 0xfb, 0x20, 0x76,
0x16, 0x9c, 0x43, 0x2e, 0xc8, 0xb6, 0x34, 0x48, 0x67, 0x5b, 0x3a, 0xa8, 0xa7, 0x3e, 0xfb, 0x20,
0xb6, 0xb3, 0x8c, 0xac, 0x4f, 0x2d, 0xee, 0xac, 0x8f, 0xc6, 0x94, 0x87, 0x24, 0xfa, 0x75, 0xeb,
0x89, 0x89, 0x83, 0xfc, 0x82, 0x0d, 0xe9, 0x67, 0x21, 0xe5, 0xda, 0xc7, 0xc3, 0x4e, 0xce, 0x53,
0x8d, 0x17, 0x6f, 0x0a, 0xc6, 0xe9, 0x66, 0xa9, 0xc5, 0x9d, 0xd5, 0xd0, 0x98, 0xf2, 0xf0, 0x47,
0xd1, 0x3b, 0x6a, 0x94, 0x6c, 0xe6, 0xb3, 0x7b, 0xde, 0x21, 0x14, 0x4f, 0x68, 0xf7, 0x3b, 0x28,
0x33, 0x38, 0x28, 0x99, 0x1a, 0x7c, 0x3e, 0xf2, 0xea, 0xa1, 0xa1, 0xe7, 0x5e, 0x18, 0x6a, 0xd9,
0xde, 0x85, 0x0c, 0x48, 0xdb, 0xb5, 0xb0, 0xc3, 0xb6, 0x86, 0x94, 0x6d, 0x1e, 0xfd, 0x96, 0x7e,
0x2c, 0xd5, 0x3c, 0x2a, 0xe5, 0xd5, 0x20, 0xbd, 0x4e, 0xd4, 0xdb, 0x86, 0xb4, 0xaf, 0x27, 0xfd,
0xe0, 0x56, 0x7d, 0x54, 0x0f, 0xf4, 0xd7, 0x07, 0xf5, 0xbf, 0x7b, 0x61, 0x48, 0xd9, 0xfe, 0xfb,
0x41, 0xf4, 0x03, 0x25, 0x7b, 0x91, 0xc7, 0xe7, 0x19, 0xc8, 0x29, 0xf1, 0x18, 0xc4, 0x35, 0xe3,
0x57, 0xe3, 0x55, 0x9e, 0x10, 0xd3, 0xbf, 0x1f, 0xee, 0x98, 0xfe, 0x49, 0x25, 0x2b, 0xe3, 0x53,
0x15, 0x15, 0xac, 0xc0, 0x19, 0x5f, 0x53, 0x03, 0xc1, 0x0a, 0x2a, 0xe3, 0x73, 0x91, 0x96, 0xd5,
0xa3, 0x6a, 0xd8, 0xf4, 0x5b, 0x3d, 0xb2, 0xc7, 0xc9, 0xbb, 0x21, 0xc4, 0x0c, 0x5b, 0x4d, 0x00,
0xb3, 0xfc, 0x22, 0x9d, 0x9d, 0x15, 0xd3, 0x2a, 0x8c, 0x1f, 0xf9, 0x23, 0xd4, 0x42, 0x88, 0x61,
0x8b, 0x40, 0x95, 0xb7, 0x7f, 0x34, 0x89, 0x91, 0xea, 0x4a, 0x7b, 0x9c, 0xcd, 0x0f, 0x61, 0x16,
0x27, 0x2b, 0xd5, 0xff, 0x3f, 0x09, 0x75, 0x3c, 0x4c, 0xeb, 0x42, 0x7c, 0x7a, 0x43, 0x2d, 0x55,
0x9e, 0xff, 0x1c, 0x44, 0xf7, 0x9a, 0xea, 0x5f, 0xc6, 0xf9, 0x0c, 0x54, 0x7b, 0xd6, 0xa5, 0xdf,
0xce, 0xa7, 0xa7, 0x50, 0x8a, 0x98, 0x8b, 0xe1, 0x17, 0xfe, 0x4a, 0x86, 0x74, 0x74, 0xd9, 0x7e,
0xf4, 0xad, 0x74, 0x4d, 0xab, 0x8f, 0xab, 0x81, 0x4d, 0x0d, 0x01, 0x6e, 0xab, 0x4b, 0x09, 0x1e,
0x00, 0xee, 0x86, 0x10, 0xd3, 0xea, 0x52, 0x70, 0x90, 0x2f, 0x53, 0x01, 0xfb, 0x90, 0x03, 0x6f,
0xb7, 0x7a, 0xad, 0xea, 0x22, 0x44, 0xab, 0x13, 0xa8, 0x19, 0x6c, 0x1c, 0x6f, 0x7a, 0x72, 0x5c,
0x0f, 0x18, 0x69, 0x4d, 0x8f, 0x4f, 0xfa, 0xc1, 0x66, 0x75, 0x67, 0xf9, 0x3c, 0x85, 0x25, 0xbb,
0xc2, 0xab, 0x3b, 0xdb, 0x44, 0x0d, 0x10, 0xab, 0x3b, 0x2f, 0x68, 0x66, 0x30, 0xcb, 0xcf, 0xab,
0x14, 0xae, 0xd1, 0x0c, 0x66, 0x2b, 0x57, 0x62, 0x62, 0x06, 0xf3, 0x60, 0xca, 0xc3, 0x71, 0xf4,
0x2b, 0x52, 0xf8, 0x07, 0x2c, 0xcd, 0x87, 0xb7, 0x3d, 0x4a, 0x95, 0x40, 0x5b, 0xbd, 0x43, 0x03,
0xa8, 0xc4, 0xd5, 0xaf, 0x3b, 0x71, 0x9e, 0x40, 0xe6, 0x2d, 0xb1, 0x11, 0x07, 0x4b, 0xec, 0x60,
0x26, 0x75, 0x90, 0xc2, 0x6a, 0xfc, 0x1a, 0x5f, 0xc6, 0x3c, 0xcd, 0x67, 0x43, 0x9f, 0xae, 0x25,
0x27, 0x52, 0x07, 0x1f, 0x87, 0x42, 0x58, 0x29, 0x6e, 0x17, 0x05, 0xaf, 0x86, 0x45, 0x5f, 0x08,
0xbb, 0x48, 0x30, 0x84, 0x5b, 0xa8, 0xdf, 0xdb, 0x2e, 0x24, 0x59, 0x9a, 0x07, 0xbd, 0x29, 0xa4,
0x8f, 0x37, 0x83, 0xa2, 0xe0, 0x3d, 0x84, 0x78, 0x09, 0x4d, 0xcd, 0x7c, 0x4f, 0xc6, 0x06, 0x82,
0xc1, 0x8b, 0x40, 0xb3, 0x4e, 0x93, 0xe2, 0xa3, 0xf8, 0x0a, 0xaa, 0x07, 0x0c, 0xd5, 0xbc, 0x36,
0xf4, 0xe9, 0x3b, 0x04, 0xb1, 0x4e, 0xf3, 0x93, 0xca, 0xd5, 0x22, 0x7a, 0x4f, 0xca, 0x4f, 0x62,
0x2e, 0xd2, 0x24, 0x2d, 0xe2, 0xbc, 0xc9, 0xff, 0x7d, 0xfd, 0xba, 0x45, 0x69, 0x97, 0x1b, 0x3d,
0x69, 0xe5, 0xf6, 0xdf, 0x07, 0xd1, 0x87, 0xd8, 0xef, 0x09, 0xf0, 0x79, 0x2a, 0x97, 0x91, 0x65,
0x3d, 0x08, 0x0f, 0x3f, 0x0f, 0x1b, 0x6d, 0x29, 0xe8, 0xd2, 0xfc, 0xf0, 0xe6, 0x8a, 0xaa, 0x60,
0x7f, 0x18, 0x45, 0xf5, 0x72, 0x45, 0x2e, 0x29, 0xdd, 0x5e, 0xab, 0xd6, 0x31, 0xce, 0x7a, 0xf2,
0xc3, 0x00, 0x61, 0xa6, 0x8a, 0xfa, 0x77, 0xb9, 0x52, 0x1e, 0x7a, 0x35, 0xa4, 0x88, 0x98, 0x2a,
0x10, 0x82, 0x0b, 0x3a, 0xbe, 0x64, 0xd7, 0xfe, 0x82, 0x56, 0x92, 0x70, 0x41, 0x15, 0x61, 0xf6,
0xae, 0x54, 0x41, 0x7d, 0x7b, 0x57, 0x4d, 0x31, 0x42, 0x7b, 0x57, 0x98, 0x51, 0x86, 0x59, 0xf4,
0x9b, 0xb6, 0xe1, 0xe7, 0x8c, 0x5d, 0xcd, 0x63, 0x7e, 0x35, 0x7c, 0x4c, 0x2b, 0x37, 0x8c, 0x76,
0xb4, 0xde, 0x8b, 0x35, 0xc3, 0x82, 0xed, 0xb0, 0x4a, 0x34, 0xce, 0x78, 0x86, 0x86, 0x05, 0xc7,
0x86, 0x42, 0x88, 0x61, 0x81, 0x40, 0xcd, 0xc8, 0x6d, 0x7b, 0x1b, 0x03, 0x5e, 0x2d, 0x39, 0xea,
0x63, 0xa0, 0x56, 0x4b, 0x1e, 0x0c, 0x87, 0xd0, 0x3e, 0x8f, 0x8b, 0x4b, 0x7f, 0x08, 0x49, 0x51,
0x38, 0x84, 0x1a, 0x04, 0xb7, 0xf7, 0x18, 0x62, 0x9e, 0x5c, 0xfa, 0xdb, 0xbb, 0x96, 0x85, 0xdb,
0x5b, 0x33, 0xb8, 0xbd, 0x6b, 0xc1, 0xeb, 0x54, 0x5c, 0x1e, 0x81, 0x88, 0xfd, 0xed, 0xed, 0x32,
0xe1, 0xf6, 0x6e, 0xb1, 0x26, 0x93, 0xb1, 0x1d, 0x8e, 0x17, 0xe7, 0x65, 0xc2, 0xd3, 0x73, 0x18,
0x06, 0xac, 0x68, 0x88, 0xc8, 0x64, 0x48, 0xd8, 0x0c, 0xd2, 0xca, 0x67, 0x23, 0x3b, 0x98, 0x96,
0x68, 0x90, 0x6e, 0x6c, 0x58, 0x04, 0x31, 0x48, 0xfb, 0x49, 0x5c, 0xbd, 0x7d, 0xce, 0x16, 0x45,
0xd9, 0x51, 0x3d, 0x04, 0x85, 0xab, 0xd7, 0x86, 0x95, 0xcf, 0x37, 0xd1, 0x6f, 0xdb, 0x8f, 0xf4,
0x2c, 0x2f, 0xb5, 0xd7, 0x0d, 0xfa, 0x39, 0x59, 0x18, 0xb1, 0x2d, 0x15, 0xc0, 0x4d, 0x9a, 0xd2,
0x78, 0x16, 0xbb, 0x20, 0xe2, 0x34, 0x2b, 0x87, 0x0f, 0xfc, 0x36, 0x1a, 0x39, 0x91, 0xa6, 0xf8,
0x38, 0xdc, 0x67, 0x77, 0x17, 0x45, 0x96, 0x26, 0xed, 0xfd, 0x49, 0xa5, 0xab, 0xc5, 0xe1, 0x3e,
0x6b, 0x63, 0x78, 0x0c, 0x1a, 0x83, 0xa8, 0xff, 0x33, 0x59, 0x15, 0xe0, 0x1f, 0x83, 0x1c, 0x24,
0x3c, 0x06, 0x61, 0x14, 0xd7, 0x67, 0x0c, 0xe2, 0x30, 0x5e, 0xb1, 0x05, 0x31, 0x06, 0x69, 0x71,
0xb8, 0x3e, 0x36, 0x66, 0x32, 0x05, 0xed, 0xe1, 0x20, 0x17, 0xc0, 0xf3, 0x38, 0xdb, 0xcb, 0xe2,
0x59, 0x39, 0x24, 0xfa, 0x8d, 0x4b, 0x11, 0x99, 0x02, 0x4d, 0x7b, 0x1e, 0xe3, 0x41, 0xb9, 0x17,
0x2f, 0x19, 0x4f, 0x05, 0xfd, 0x18, 0x0d, 0xd2, 0xf9, 0x18, 0x1d, 0xd4, 0xeb, 0x6d, 0x9b, 0x27,
0x97, 0xe9, 0x12, 0xa6, 0x01, 0x6f, 0x0d, 0xd2, 0xc3, 0x9b, 0x85, 0x7a, 0x1a, 0x6d, 0xcc, 0x16,
0x3c, 0x01, 0xb2, 0xd1, 0x6a, 0x71, 0x67, 0xa3, 0x69, 0x4c, 0x79, 0xf8, 0xeb, 0x41, 0xf4, 0x3b,
0xb5, 0xd4, 0xde, 0x34, 0xdc, 0x8d, 0xcb, 0xcb, 0x73, 0x16, 0xf3, 0xe9, 0xf0, 0x63, 0x9f, 0x1d,
0x2f, 0xaa, 0x5d, 0x3f, 0xbd, 0x89, 0x0a, 0x7e, 0xac, 0x87, 0x69, 0x69, 0xf5, 0x38, 0xef, 0x63,
0x75, 0x90, 0xf0, 0x63, 0xc5, 0x28, 0x1e, 0x40, 0xa4, 0xbc, 0x5e, 0xa0, 0x3f, 0x20, 0xf5, 0xdd,
0x55, 0xfa, 0xc3, 0x4e, 0x0e, 0x8f, 0x8f, 0x95, 0xd0, 0x8d, 0x96, 0x0d, 0xca, 0x86, 0x3f, 0x62,
0x46, 0x7d, 0x71, 0xd2, 0xb3, 0xee, 0x15, 0x61, 0xcf, 0xad, 0x9e, 0x31, 0xea, 0x8b, 0x13, 0x9e,
0xad, 0x61, 0x2d, 0xe4, 0xd9, 0x33, 0xb4, 0x8d, 0xfa, 0xe2, 0x38, 0xa3, 0x50, 0x4c, 0x33, 0x2f,
0x3c, 0x0e, 0xd8, 0xc1, 0x73, 0xc3, 0x7a, 0x2f, 0x16, 0x47, 0xec, 0x76, 0x51, 0x64, 0xab, 0x09,
0xcc, 0x8b, 0x8c, 0x8c, 0x58, 0x07, 0x09, 0x47, 0x2c, 0x46, 0x71, 0x7e, 0x37, 0x61, 0x55, 0xf6,
0xe8, 0xcd, 0xef, 0xa4, 0x28, 0x9c, 0xdf, 0x35, 0x08, 0xce, 0x50, 0x26, 0x6c, 0x87, 0x65, 0x19,
0x24, 0xa2, 0x7d, 0xdc, 0xa7, 0x35, 0x0d, 0x11, 0xce, 0x50, 0x10, 0x69, 0x56, 0xc6, 0xcd, 0x6a,
0x24, 0xe6, 0xf0, 0x7c, 0x75, 0x98, 0xe6, 0x57, 0x43, 0xff, 0x64, 0x6c, 0x00, 0x62, 0x65, 0xec,
0x05, 0xf1, 0xaa, 0xe7, 0x2c, 0x9f, 0x32, 0xff, 0xaa, 0xa7, 0x92, 0x84, 0x57, 0x3d, 0x8a, 0xc0,
0x26, 0x4f, 0x81, 0x32, 0x59, 0x49, 0xc2, 0x26, 0x15, 0xe1, 0x1b, 0x80, 0xd4, 0xfe, 0x29, 0x39,
0x00, 0xa1, 0x1d, 0xd3, 0x87, 0x9d, 0x1c, 0x8e, 0xd0, 0x66, 0xf9, 0xb3, 0x07, 0x22, 0xb9, 0xf4,
0x47, 0xa8, 0x83, 0x84, 0x23, 0x14, 0xa3, 0xb8, 0x4a, 0x13, 0xa6, 0x97, 0x6f, 0x0f, 0xfc, 0xf1,
0xd1, 0x5a, 0xba, 0x3d, 0xec, 0xe4, 0xf0, 0x82, 0xe4, 0x60, 0x2e, 0x9f, 0x99, 0x37, 0xc8, 0x6b,
0x59, 0x78, 0x41, 0xa2, 0x19, 0x5c, 0xfa, 0x5a, 0x50, 0x3d, 0x4e, 0x7f, 0xe9, 0x8d, 0x3c, 0x5c,
0x7a, 0x87, 0x53, 0x4e, 0xfe, 0x75, 0x10, 0xdd, 0xb6, 0xbd, 0x1c, 0xb3, 0xaa, 0x8f, 0xbc, 0x8a,
0xb3, 0x74, 0x1a, 0x0b, 0x98, 0xb0, 0x2b, 0xc8, 0xd1, 0x8e, 0x86, 0x5b, 0xda, 0x9a, 0x1f, 0x39,
0x0a, 0xc4, 0x8e, 0x46, 0x2f, 0x45, 0x1c, 0x27, 0x35, 0x7d, 0x56, 0xc2, 0x4e, 0x5c, 0x12, 0x23,
0x99, 0x83, 0x84, 0xe3, 0x04, 0xa3, 0x38, 0x4b, 0xac, 0xe5, 0x2f, 0xde, 0x14, 0xc0, 0x53, 0xc8,
0x13, 0xf0, 0x67, 0x89, 0x98, 0x0a, 0x67, 0x89, 0x1e, 0xba, 0xb5, 0xe0, 0xd7, 0x83, 0x53, 0xfb,
0xc4, 0x1e, 0x13, 0x81, 0x13, 0x7b, 0x02, 0xc5, 0x95, 0x34, 0x80, 0x77, 0xd3, 0xac, 0x65, 0x25,
0xb8, 0x69, 0x46, 0xd3, 0xad, 0x6d, 0x14, 0xcd, 0x8c, 0xab, 0x6e, 0xd2, 0x51, 0xf4, 0xb1, 0xdd,
0x5d, 0xd6, 0x7b, 0xb1, 0xfe, 0x7d, 0x9b, 0x53, 0xc8, 0x62, 0x39, 0x85, 0x04, 0x36, 0x47, 0x1a,
0xa6, 0xcf, 0xbe, 0x8d, 0xc5, 0x2a, 0x87, 0x7f, 0x39, 0x88, 0x3e, 0xf0, 0x79, 0x7c, 0x59, 0x48,
0xbf, 0x5b, 0xdd, 0xb6, 0x6a, 0x92, 0xb8, 0x92, 0x10, 0xd6, 0x50, 0x65, 0xf8, 0xd3, 0xe8, 0xfd,
0x46, 0x64, 0x6e, 0x2c, 0xa8, 0x02, 0xb8, 0x69, 0x8b, 0x2e, 0x3f, 0xe6, 0xb4, 0xfb, 0xcd, 0xde,
0xbc, 0x59, 0x11, 0xb8, 0xe5, 0x2a, 0xd1, 0x8a, 0x40, 0xdb, 0x50, 0x62, 0x62, 0x45, 0xe0, 0xc1,
0xf0, 0x4c, 0xdd, 0x20, 0x55, 0x3f, 0xf1, 0x8d, 0x71, 0xda, 0x84, 0xdd, 0x4b, 0xd6, 0xba, 0x41,
0x1c, 0x3b, 0x8d, 0x58, 0x25, 0xe2, 0x8f, 0x43, 0x16, 0x50, 0x32, 0xbe, 0xde, 0x8b, 0x55, 0x0e,
0xff, 0x3c, 0xfa, 0x5e, 0xab, 0x62, 0x7b, 0x10, 0x8b, 0x05, 0x87, 0xe9, 0x70, 0xb3, 0xa3, 0xdc,
0x0d, 0xa8, 0x5d, 0x6f, 0xf5, 0x57, 0x50, 0xfe, 0xff, 0x76, 0x10, 0x7d, 0xdf, 0xe5, 0xea, 0x26,
0xd6, 0x65, 0x78, 0x1a, 0x32, 0xe9, 0xb2, 0xba, 0x18, 0xcf, 0x6e, 0xa4, 0xd3, 0x5a, 0xf4, 0xd9,
0x81, 0xbc, 0xbd, 0x8c, 0xd3, 0x4c, 0x1e, 0x24, 0x7c, 0x1c, 0x32, 0xea, 0xa0, 0xc1, 0x45, 0x1f,
0xa9, 0xd2, 0x1a, 0x25, 0x65, 0x7f, 0xb3, 0x16, 0x0b, 0x4f, 0xe8, 0x5e, 0xe9, 0x59, 0x2b, 0x6c,
0xf4, 0xa4, 0x95, 0x5b, 0xd1, 0x6c, 0x96, 0x55, 0x3f, 0xdb, 0x41, 0xee, 0xf3, 0xaa, 0x54, 0x3d,
0x91, 0xbe, 0xd1, 0x93, 0x56, 0x5e, 0xff, 0x2c, 0x7a, 0xbf, 0xed, 0x55, 0x4d, 0x0a, 0x9b, 0x9d,
0xa6, 0xd0, 0xbc, 0xb0, 0xd5, 0x5f, 0xc1, 0xa4, 0xfa, 0x5f, 0xa6, 0xa5, 0x60, 0x7c, 0x35, 0xbe,
0x64, 0xd7, 0xcd, 0xad, 0x5c, 0xb7, 0xb7, 0x2a, 0x60, 0x64, 0x11, 0x44, 0xaa, 0xef, 0x27, 0x5b,
0xae, 0xcc, 0xed, 0xdd, 0x92, 0x70, 0x65, 0x11, 0x1d, 0xae, 0x5c, 0xd2, 0x8c, 0x55, 0x4d, 0xad,
0xcc, 0x55, 0xe3, 0x87, 0xfe, 0xa2, 0xb6, 0xaf, 0x1b, 0xaf, 0x75, 0x83, 0x26, 0x7b, 0x50, 0xe2,
0xdd, 0xf4, 0xe2, 0x42, 0xd7, 0xc9, 0x5f, 0x52, 0x1b, 0x21, 0xb2, 0x07, 0x02, 0x35, 0xc9, 0xe8,
0x5e, 0x9a, 0x81, 0x3c, 0x95, 0x7a, 0x79, 0x71, 0x91, 0xb1, 0x78, 0x8a, 0x92, 0xd1, 0x4a, 0x3c,
0xb2, 0xe5, 0x44, 0x32, 0xea, 0xe3, 0xcc, 0xa5, 0x9e, 0x4a, 0x7a, 0x0a, 0x09, 0xcb, 0x93, 0x34,
0xc3, 0x97, 0x94, 0xa4, 0xa6, 0x16, 0x12, 0x97, 0x7a, 0x5a, 0x90, 0x99, 0xa4, 0x2a, 0x51, 0xd5,
0xed, 0x9b, 0xf2, 0xdf, 0x6f, 0x2b, 0x5a, 0x62, 0x62, 0x92, 0xf2, 0x60, 0x66, 0x4d, 0x56, 0x09,
0xcf, 0x0a, 0x69, 0xfc, 0x4e, 0x5b, 0xab, 0x96, 0x10, 0x6b, 0x32, 0x97, 0x30, 0x6b, 0x8b, 0xea,
0xf7, 0x5d, 0x76, 0x9d, 0x4b, 0xa3, 0x77, 0xdb, 0x2a, 0x8d, 0x8c, 0x58, 0x5b, 0x60, 0x46, 0x19,
0xfe, 0x71, 0xf4, 0xcb, 0xd2, 0x30, 0x67, 0xc5, 0xf0, 0x96, 0x47, 0x81, 0x5b, 0xf7, 0x89, 0x6e,
0x93, 0x72, 0x73, 0x2d, 0x4e, 0xc7, 0xc6, 0x59, 0x19, 0xcf, 0x60, 0x78, 0x8f, 0x68, 0x71, 0x29,
0x25, 0xae, 0xc5, 0xb5, 0x29, 0x37, 0x2a, 0x8e, 0xd9, 0x54, 0x59, 0xf7, 0xd4, 0x50, 0x0b, 0x43,
0x51, 0x61, 0x43, 0xe6, 0x90, 0xe2, 0x38, 0x5e, 0xa6, 0x33, 0x3d, 0xe1, 0xd4, 0xe3, 0x56, 0x89,
0x0e, 0x29, 0x0c, 0x33, 0xb2, 0x20, 0xe2, 0x90, 0x82, 0x84, 0x95, 0xcf, 0x7f, 0x19, 0x44, 0x77,
0x0c, 0xb3, 0xdf, 0xec, 0x1d, 0x1d, 0xe4, 0x17, 0xec, 0x75, 0x2a, 0x2e, 0x0f, 0xd3, 0xfc, 0xaa,
0x1c, 0x7e, 0x46, 0x99, 0xf4, 0xf3, 0xba, 0x28, 0x9f, 0xdf, 0x58, 0xcf, 0x64, 0x90, 0xcd, 0x16,
0x8f, 0x39, 0x31, 0xac, 0x35, 0x50, 0x06, 0xa9, 0x77, 0x82, 0x30, 0x47, 0x64, 0x90, 0x21, 0xde,
0x34, 0xb1, 0x76, 0x9e, 0xb1, 0x1c, 0x37, 0xb1, 0xb1, 0x50, 0x09, 0x89, 0x26, 0x6e, 0x41, 0x66,
0x3c, 0x6e, 0x44, 0xf5, 0x6e, 0xc4, 0x76, 0x96, 0xa1, 0xf1, 0x58, 0xab, 0x6a, 0x80, 0x18, 0x8f,
0xbd, 0xa0, 0xf2, 0x73, 0x1a, 0x7d, 0xa7, 0x7a, 0xa4, 0x27, 0x1c, 0x96, 0x29, 0xe0, 0xc3, 0x6d,
0x4b, 0x42, 0xf4, 0x7f, 0x97, 0x30, 0x3d, 0xeb, 0x2c, 0x2f, 0x8b, 0x2c, 0x2e, 0x2f, 0xd5, 0x71,
0xa7, 0x5b, 0xe7, 0x46, 0x88, 0x0f, 0x3c, 0xef, 0x77, 0x50, 0x66, 0x50, 0x6f, 0x64, 0x7a, 0x88,
0x79, 0xe0, 0x57, 0x6d, 0x0d, 0x33, 0x0f, 0x3b, 0x39, 0xb3, 0xff, 0xba, 0x1f, 0x67, 0x19, 0xf0,
0x55, 0x23, 0x3b, 0x8a, 0xf3, 0xf4, 0x02, 0x4a, 0x81, 0xf6, 0x5f, 0x15, 0x35, 0xc2, 0x18, 0xb1,
0xff, 0x1a, 0xc0, 0x4d, 0x36, 0x8f, 0x3c, 0x1f, 0xe4, 0x53, 0x78, 0x83, 0xb2, 0x79, 0x6c, 0x47,
0x32, 0x44, 0x36, 0x4f, 0xb1, 0x66, 0x47, 0xf4, 0x79, 0xc6, 0x92, 0x2b, 0x35, 0x05, 0xb8, 0x0d,
0x2c, 0x25, 0x78, 0x0e, 0xb8, 0x1b, 0x42, 0xcc, 0x24, 0x20, 0x05, 0xa7, 0x50, 0x64, 0x71, 0x82,
0x6f, 0x38, 0xd4, 0x3a, 0x4a, 0x46, 0x4c, 0x02, 0x98, 0x41, 0xc5, 0x55, 0x37, 0x27, 0x7c, 0xc5,
0x45, 0x17, 0x27, 0xee, 0x86, 0x10, 0x33, 0x0d, 0x4a, 0xc1, 0xb8, 0xc8, 0x52, 0x81, 0xba, 0x41,
0xad, 0x21, 0x25, 0x44, 0x37, 0x70, 0x09, 0x64, 0xf2, 0x08, 0xf8, 0x0c, 0xbc, 0x26, 0xa5, 0x24,
0x68, 0xb2, 0x21, 0xcc, 0x45, 0xb8, 0xba, 0xee, 0xac, 0x58, 0xa1, 0x8b, 0x70, 0xaa, 0x5a, 0xac,
0x58, 0x11, 0x17, 0xe1, 0x1c, 0x00, 0x15, 0xf1, 0x24, 0x2e, 0x85, 0xbf, 0x88, 0x52, 0x12, 0x2c,
0x62, 0x43, 0x98, 0x39, 0xba, 0x2e, 0xe2, 0x42, 0xa0, 0x39, 0x5a, 0x15, 0xc0, 0x3a, 0x0f, 0xbd,
0x4d, 0xca, 0xcd, 0x48, 0x52, 0xb7, 0x0a, 0x88, 0xbd, 0x14, 0xb2, 0x69, 0x89, 0x46, 0x12, 0xf5,
0xdc, 0x1b, 0x29, 0x31, 0x92, 0xb4, 0x29, 0x14, 0x4a, 0x6a, 0xdf, 0xd8, 0x57, 0x3b, 0xb4, 0x65,
0x7c, 0x37, 0x84, 0x98, 0xf1, 0xa9, 0x29, 0xf4, 0x4e, 0xcc, 0x79, 0x5a, 0x4d, 0xfe, 0x0f, 0xfc,
0x05, 0x6a, 0xe4, 0xc4, 0xf8, 0xe4, 0xe3, 0x50, 0xf7, 0x6a, 0x06, 0x6e, 0x5f, 0xc1, 0xf0, 0xd0,
0xfd, 0x51, 0x90, 0x31, 0x19, 0xa7, 0x94, 0x58, 0x07, 0x7a, 0xbe, 0xa7, 0xe9, 0x39, 0xcf, 0x7b,
0xd0, 0x85, 0x59, 0x17, 0xd5, 0xb5, 0x8b, 0x23, 0xb6, 0x84, 0x09, 0x7b, 0xf1, 0x26, 0x2d, 0x45,
0x9a, 0xcf, 0xd4, 0xcc, 0xfd, 0x8c, 0xb0, 0xe4, 0x83, 0x89, 0x8b, 0xea, 0x9d, 0x4a, 0x26, 0x81,
0x40, 0x65, 0x39, 0x86, 0x6b, 0x6f, 0x02, 0x81, 0x2d, 0x6a, 0x8e, 0x48, 0x20, 0x42, 0xbc, 0xd9,
0x47, 0xd1, 0xce, 0xd5, 0xdb, 0x7c, 0x13, 0xd6, 0xe4, 0x72, 0x94, 0x35, 0x0c, 0x12, 0x4b, 0xd9,
0xa0, 0x82, 0x59, 0x5f, 0x6a, 0xff, 0xa6, 0x8b, 0xad, 0x11, 0x76, 0xda, 0xdd, 0xec, 0x51, 0x0f,
0xd2, 0xe3, 0xca, 0x9c, 0x4a, 0x53, 0xae, 0xda, 0x87, 0xd2, 0x8f, 0x7a, 0x90, 0xd6, 0x9e, 0x8c,
0x5d, 0xad, 0xe7, 0x71, 0x72, 0x35, 0xe3, 0x6c, 0x91, 0x4f, 0x77, 0x58, 0xc6, 0x38, 0xda, 0x93,
0x71, 0x4a, 0x8d, 0x50, 0x62, 0x4f, 0xa6, 0x43, 0xc5, 0x64, 0x70, 0x76, 0x29, 0xb6, 0xb3, 0x74,
0x86, 0x57, 0xd4, 0x8e, 0x21, 0x09, 0x10, 0x19, 0x9c, 0x17, 0xf4, 0x04, 0x51, 0xbd, 0xe2, 0x16,
0x69, 0x12, 0x67, 0xb5, 0xbf, 0x4d, 0xda, 0x8c, 0x03, 0x76, 0x06, 0x91, 0x47, 0xc1, 0x53, 0xcf,
0xc9, 0x82, 0xe7, 0x07, 0xb9, 0x60, 0x64, 0x3d, 0x1b, 0xa0, 0xb3, 0x9e, 0x16, 0x88, 0x86, 0xd5,
0x09, 0xbc, 0xa9, 0x4a, 0x53, 0xfd, 0xe3, 0x1b, 0x56, 0xab, 0xdf, 0x47, 0x4a, 0x1e, 0x1a, 0x56,
0x11, 0x87, 0x2a, 0xa3, 0x9c, 0xd4, 0x01, 0x13, 0xd0, 0x76, 0xc3, 0x64, 0xad, 0x1b, 0xf4, 0xfb,
0x19, 0x8b, 0x55, 0x06, 0x21, 0x3f, 0x12, 0xe8, 0xe3, 0xa7, 0x01, 0xcd, 0x76, 0x8b, 0x53, 0x9f,
0x4b, 0x48, 0xae, 0x5a, 0x97, 0x6c, 0xdc, 0x82, 0xd6, 0x08, 0xb1, 0xdd, 0x42, 0xa0, 0xfe, 0x26,
0x3a, 0x48, 0x58, 0x1e, 0x6a, 0xa2, 0x4a, 0xde, 0xa7, 0x89, 0x14, 0x67, 0x16, 0xbf, 0x5a, 0xaa,
0x22, 0xb3, 0x6e, 0xa6, 0x75, 0xc2, 0x82, 0x0d, 0x11, 0x8b, 0x5f, 0x12, 0x36, 0x39, 0x39, 0xf6,
0x79, 0xd4, 0xbe, 0x55, 0xdb, 0xb2, 0x72, 0x44, 0xdf, 0xaa, 0xa5, 0x58, 0xba, 0x92, 0x75, 0x8c,
0x74, 0x58, 0x71, 0xe3, 0xe4, 0x49, 0x3f, 0xd8, 0x2c, 0x79, 0x1c, 0x9f, 0x3b, 0x19, 0xc4, 0xbc,
0xf6, 0xba, 0x11, 0x30, 0x64, 0x30, 0x62, 0xc9, 0x13, 0xc0, 0xd1, 0x10, 0xe6, 0x78, 0xde, 0x61,
0xb9, 0x80, 0x5c, 0xf8, 0x86, 0x30, 0xd7, 0x98, 0x02, 0x43, 0x43, 0x18, 0xa5, 0x80, 0xe2, 0x56,
0xee, 0x07, 0x81, 0x38, 0x8e, 0xe7, 0xde, 0x8c, 0xad, 0xde, 0xeb, 0xa9, 0xe5, 0xa1, 0xb8, 0x45,
0x9c, 0x75, 0xe0, 0x66, 0x7b, 0x99, 0xc4, 0x7c, 0xa6, 0x77, 0x37, 0xa6, 0xc3, 0x2d, 0xda, 0x8e,
0x4b, 0x12, 0x07, 0x6e, 0x61, 0x0d, 0x34, 0xec, 0x1c, 0xcc, 0xe3, 0x99, 0xae, 0xa9, 0xa7, 0x06,
0x52, 0xde, 0xaa, 0xea, 0x5a, 0x37, 0x88, 0xfc, 0xbc, 0x4a, 0xa7, 0xc0, 0x02, 0x7e, 0xa4, 0xbc,
0x8f, 0x1f, 0x0c, 0xa2, 0xec, 0xad, 0xaa, 0x77, 0xbd, 0xa2, 0xdb, 0xce, 0xa7, 0x6a, 0x1d, 0x3b,
0x22, 0x1e, 0x0f, 0xe2, 0x42, 0xd9, 0x1b, 0xc1, 0xa3, 0x3e, 0xda, 0x6c, 0xd0, 0x86, 0xfa, 0xa8,
0xde, 0x7f, 0xed, 0xd3, 0x47, 0x7d, 0xb0, 0xf2, 0xf9, 0x53, 0xd5, 0x47, 0x77, 0x63, 0x11, 0x57,
0x79, 0xfb, 0xab, 0x14, 0xae, 0xd5, 0x42, 0xd8, 0x53, 0xdf, 0x86, 0x1a, 0xc9, 0xd7, 0xa9, 0xd0,
0xaa, 0x78, 0xb3, 0x37, 0x1f, 0xf0, 0xad, 0x56, 0x08, 0x9d, 0xbe, 0xd1, 0x52, 0x61, 0xb3, 0x37,
0x1f, 0xf0, 0xad, 0xde, 0xd3, 0xec, 0xf4, 0x8d, 0x5e, 0xd6, 0xdc, 0xec, 0xcd, 0x2b, 0xdf, 0x7f,
0xd5, 0x74, 0x5c, 0xdb, 0x79, 0x95, 0x87, 0x25, 0x22, 0x5d, 0x82, 0x2f, 0x9d, 0x74, 0xed, 0x69,
0x34, 0x94, 0x4e, 0xd2, 0x2a, 0xd6, 0xc7, 0x3d, 0x7c, 0xa5, 0x38, 0x61, 0x65, 0x2a, 0x0f, 0xcc,
0x9f, 0xf5, 0x30, 0xda, 0xc0, 0xa1, 0x45, 0x53, 0x48, 0xc9, 0x1c, 0x37, 0x3a, 0xa8, 0xb9, 0x53,
0xfb, 0x24, 0x60, 0xaf, 0x7d, 0xb5, 0x76, 0xa3, 0x27, 0x6d, 0x0e, 0xfe, 0x1c, 0xc6, 0x3e, 0x71,
0x0c, 0xb5, 0xaa, 0xf7, 0xd0, 0x71, 0xab, 0xbf, 0x82, 0x72, 0xff, 0x37, 0xcd, 0xba, 0x02, 0xfb,
0x57, 0x9d, 0xe0, 0x69, 0x1f, 0x8b, 0xa8, 0x23, 0x3c, 0xbb, 0x91, 0x8e, 0x2a, 0xc8, 0x7f, 0x0c,
0xa2, 0xbb, 0xde, 0x82, 0xb8, 0x67, 0xcf, 0xbf, 0xdb, 0xc7, 0xb6, 0xff, 0x0c, 0xfa, 0x8b, 0x6f,
0xa3, 0xaa, 0x4a, 0xf7, 0x0f, 0xcd, 0xf2, 0xbe, 0xd1, 0x90, 0xef, 0x3d, 0xbc, 0xe4, 0x53, 0xe0,
0xaa, 0xc7, 0x86, 0x82, 0xce, 0xc0, 0xb8, 0xdf, 0x7e, 0x7a, 0x43, 0x2d, 0xeb, 0x43, 0x34, 0x0e,
0xac, 0xde, 0x39, 0xb3, 0xca, 0x13, 0xb2, 0x6c, 0xd1, 0xb8, 0x40, 0x9f, 0xdd, 0x54, 0x8d, 0xea,
0xc9, 0x16, 0x2c, 0xdf, 0x6b, 0x7f, 0xd6, 0xd3, 0xb0, 0xf3, 0xa6, 0xfb, 0x27, 0x37, 0x53, 0x52,
0x65, 0xf9, 0xaf, 0x41, 0x74, 0xdf, 0x61, 0xcd, 0x69, 0x07, 0xda, 0x93, 0xf9, 0x51, 0xc0, 0x3e,
0xa5, 0xa4, 0x0b, 0xf7, 0x7b, 0xdf, 0x4e, 0xd9, 0x7c, 0xb5, 0xc5, 0x51, 0xd9, 0x4b, 0x33, 0x01,
0xbc, 0xfd, 0xd5, 0x16, 0xd7, 0x6e, 0x4d, 0x8d, 0xe8, 0xaf, 0xb6, 0x04, 0x70, 0xeb, 0xab, 0x2d,
0x1e, 0xcf, 0xde, 0xaf, 0xb6, 0x78, 0xad, 0x05, 0xbf, 0xda, 0x12, 0xd6, 0xa0, 0x26, 0x9f, 0xa6,
0x08, 0xf5, 0xae, 0x7a, 0x2f, 0x8b, 0xee, 0x26, 0xfb, 0xd3, 0x9b, 0xa8, 0x10, 0xd3, 0x6f, 0xcd,
0xc9, 0x1b, 0x71, 0x3d, 0x9e, 0xa9, 0x73, 0x2b, 0x6e, 0xb3, 0x37, 0xaf, 0x7c, 0xff, 0x44, 0xad,
0xbd, 0xf4, 0x64, 0xc3, 0xb8, 0xfc, 0x62, 0xcf, 0x7a, 0x68, 0xf2, 0xa8, 0x2c, 0xd8, 0x2d, 0xff,
0xa4, 0x1f, 0x4c, 0x54, 0xb7, 0x22, 0x54, 0xa3, 0x8f, 0xba, 0x0c, 0xa1, 0x26, 0xdf, 0xec, 0xcd,
0x13, 0x93, 0x5c, 0xed, 0xbb, 0x6e, 0xed, 0x1e, 0xc6, 0xdc, 0xb6, 0xde, 0xea, 0xaf, 0xa0, 0xdc,
0x2f, 0x55, 0x52, 0x6b, 0xbb, 0x97, 0xed, 0xbc, 0xd1, 0x65, 0x6a, 0xec, 0x34, 0xf3, 0xa8, 0x2f,
0x1e, 0x4a, 0x6f, 0xec, 0x09, 0xbe, 0x2b, 0xbd, 0xf1, 0x4e, 0xf2, 0x9f, 0xdc, 0x4c, 0x49, 0x95,
0xe5, 0x9f, 0x07, 0xd1, 0x6d, 0xb2, 0x2c, 0x2a, 0x0e, 0x3e, 0xeb, 0x6b, 0x19, 0xc5, 0xc3, 0xe7,
0x37, 0xd6, 0x53, 0x85, 0xfa, 0xb7, 0x41, 0x74, 0x27, 0x50, 0xa8, 0x3a, 0x40, 0x6e, 0x60, 0xdd,
0x0d, 0x94, 0x1f, 0xde, 0x5c, 0x91, 0x9a, 0xee, 0x6d, 0x7c, 0xdc, 0xfe, 0x9c, 0x49, 0xc0, 0xf6,
0x98, 0xfe, 0x9c, 0x49, 0xb7, 0x16, 0xde, 0x82, 0xaa, 0x92, 0x12, 0xb5, 0x32, 0xf2, 0x6d, 0x41,
0xc9, 0x9c, 0x05, 0xad, 0x88, 0x1e, 0x76, 0x72, 0x3e, 0x27, 0x2f, 0xde, 0x14, 0x71, 0x3e, 0xa5,
0x9d, 0xd4, 0xf2, 0x6e, 0x27, 0x9a, 0xc3, 0x5b, 0x77, 0x95, 0xf4, 0x94, 0x35, 0xcb, 0xbc, 0x47,
0x94, 0xbe, 0x46, 0x82, 0x5b, 0x77, 0x2d, 0x94, 0xf0, 0xa6, 0x72, 0xda, 0x90, 0x37, 0x94, 0xca,
0x3e, 0xee, 0x83, 0xa2, 0x05, 0x84, 0xf6, 0xa6, 0x4f, 0x04, 0x9e, 0x84, 0xac, 0xb4, 0x4e, 0x05,
0x36, 0x7a, 0xd2, 0x84, 0xdb, 0x31, 0x88, 0x2f, 0x21, 0x9e, 0x02, 0x0f, 0xba, 0xd5, 0x54, 0x2f,
0xb7, 0x36, 0xed, 0x73, 0xbb, 0xc3, 0xb2, 0xc5, 0x3c, 0x57, 0x8d, 0x49, 0xba, 0xb5, 0xa9, 0x6e,
0xb7, 0x88, 0xc6, 0x9b, 0x96, 0xc6, 0xad, 0x4c, 0x2f, 0x1f, 0x87, 0xcd, 0x38, 0x59, 0xe5, 0x7a,
0x2f, 0x96, 0xae, 0xa7, 0x0a, 0xa3, 0x8e, 0x7a, 0xa2, 0x48, 0xda, 0xe8, 0x49, 0xe3, 0xdd, 0x43,
0xcb, 0xad, 0x8e, 0xa7, 0xcd, 0x0e, 0x5b, 0xad, 0x90, 0xda, 0xea, 0xaf, 0x80, 0xf7, 0x6a, 0x55,
0x54, 0x55, 0xeb, 0xa2, 0xbd, 0x34, 0xcb, 0x86, 0xeb, 0x81, 0x30, 0x69, 0xa0, 0xe0, 0x5e, 0xad,
0x07, 0x26, 0x22, 0xb9, 0xd9, 0xdb, 0xcc, 0x87, 0x5d, 0x76, 0x24, 0xd5, 0x2b, 0x92, 0x6d, 0x1a,
0xed, 0xb7, 0x59, 0x8f, 0x5a, 0xd7, 0x76, 0x14, 0x7e, 0x70, 0xad, 0x0a, 0x6f, 0xf6, 0xe6, 0xd1,
0x65, 0x00, 0x49, 0xc9, 0x99, 0xe5, 0x1e, 0x65, 0xc2, 0x99, 0x49, 0xee, 0x77, 0x50, 0x68, 0xcf,
0xb2, 0xee, 0x46, 0xaf, 0xd3, 0xe9, 0x0c, 0x84, 0xf7, 0x1c, 0xcb, 0x06, 0x82, 0xe7, 0x58, 0x08,
0x44, 0x4d, 0x57, 0xff, 0xae, 0x37, 0x6b, 0x0f, 0xa6, 0xbe, 0xa6, 0x53, 0xca, 0x16, 0x15, 0x6a,
0x3a, 0x2f, 0x8d, 0x46, 0x03, 0xed, 0x56, 0xbd, 0xba, 0xfe, 0x38, 0x64, 0x06, 0xbd, 0xbf, 0xbe,
0xde, 0x8b, 0x45, 0x33, 0x8a, 0x71, 0x98, 0xce, 0x53, 0xe1, 0x9b, 0x51, 0x2c, 0x1b, 0x15, 0x12,
0x9a, 0x51, 0xda, 0x28, 0x55, 0xbd, 0x2a, 0x47, 0x38, 0x98, 0x86, 0xab, 0x57, 0x33, 0xfd, 0xaa,
0xa7, 0xd9, 0xd6, 0xb1, 0x6b, 0xae, 0x43, 0x46, 0x5c, 0xaa, 0xc5, 0xb2, 0x27, 0xb6, 0xe5, 0xcb,
0x95, 0x18, 0x0c, 0x8d, 0x3a, 0x94, 0x02, 0x3e, 0x4e, 0xa8, 0xb8, 0xe6, 0x64, 0xb8, 0x28, 0x20,
0xe6, 0x71, 0x9e, 0x78, 0x17, 0xa7, 0xd2, 0x60, 0x8b, 0x0c, 0x2d, 0x4e, 0x49, 0x0d, 0x74, 0xa8,
0xef, 0xbe, 0x16, 0xe9, 0xe9, 0x0a, 0xfa, 0xfd, 0x43, 0xf7, 0xad, 0xc8, 0x47, 0x3d, 0x48, 0x7c,
0xa8, 0xdf, 0x00, 0x7a, 0x5b, 0xbe, 0x76, 0xfa, 0x71, 0xc0, 0x94, 0x8b, 0x86, 0x16, 0xc2, 0xb4,
0x0a, 0x0a, 0x6a, 0x9d, 0xe0, 0x82, 0xf8, 0x31, 0xac, 0x7c, 0x41, 0x6d, 0xf2, 0x53, 0x89, 0x84,
0x82, 0xba, 0x8d, 0xa2, 0x3c, 0xd3, 0x5e, 0x07, 0x3d, 0x08, 0xe8, 0xdb, 0x4b, 0x9f, 0x87, 0x9d,
0x1c, 0xea, 0x39, 0xbb, 0xe9, 0xd2, 0x39, 0xc5, 0xf0, 0x14, 0x74, 0x37, 0x5d, 0xfa, 0x0f, 0x31,
0xd6, 0x7b, 0xb1, 0xf8, 0xc2, 0x40, 0x2c, 0xe0, 0x4d, 0x73, 0x92, 0xef, 0x29, 0xae, 0x94, 0xb7,
0x8e, 0xf2, 0xd7, 0xba, 0x41, 0x73, 0x3d, 0xf7, 0x84, 0xb3, 0x04, 0xca, 0x52, 0x7d, 0xe3, 0xcd,
0xbd, 0xff, 0xa4, 0x64, 0x23, 0xf4, 0x85, 0xb7, 0x7b, 0x61, 0x48, 0xd9, 0xfe, 0x32, 0x7a, 0xfb,
0x90, 0xcd, 0xc6, 0x90, 0x4f, 0x87, 0x3f, 0x70, 0x2f, 0xc4, 0xb2, 0xd9, 0xa8, 0xfa, 0x59, 0xdb,
0xbb, 0x45, 0x89, 0xcd, 0x95, 0xbe, 0x5d, 0x38, 0x5f, 0xcc, 0xc6, 0x22, 0x16, 0xe8, 0x4a, 0x9f,
0xfc, 0x7d, 0x54, 0x09, 0x88, 0x2b, 0x7d, 0x0e, 0x80, 0xec, 0x4d, 0x38, 0x80, 0xd7, 0x5e, 0x25,
0x08, 0xda, 0x53, 0x80, 0x99, 0x75, 0xb5, 0xbd, 0x2a, 0xb1, 0xc5, 0x57, 0xf0, 0x8c, 0x8e, 0x94,
0x12, 0xb3, 0x6e, 0x9b, 0x32, 0xc1, 0x50, 0x57, 0x5f, 0x7e, 0xd1, 0x62, 0x31, 0x9f, 0xc7, 0x7c,
0x85, 0x82, 0x41, 0xd5, 0xd2, 0x02, 0x88, 0x60, 0xf0, 0x82, 0x26, 0xca, 0x9b, 0xc7, 0x9c, 0x5c,
0xed, 0x33, 0xce, 0x16, 0x22, 0xcd, 0x01, 0x7f, 0xd5, 0x40, 0x3f, 0x50, 0x9b, 0x21, 0xa2, 0x9c,
0x62, 0x4d, 0x56, 0x28, 0x89, 0xfa, 0x76, 0xa0, 0xfc, 0x52, 0x6a, 0x29, 0x18, 0xc7, 0xa7, 0x83,
0xb5, 0x15, 0x0c, 0x11, 0x59, 0x21, 0x09, 0xa3, 0xb6, 0x3f, 0x49, 0xf3, 0x99, 0xb7, 0xed, 0x4f,
0xec, 0xef, 0x0c, 0xde, 0xa1, 0x01, 0x33, 0xbe, 0xd7, 0x0f, 0xad, 0xfe, 0x72, 0x90, 0x7a, 0x4b,
0xd2, 0xfb, 0xd0, 0x6d, 0x82, 0x18, 0xdf, 0xfd, 0x24, 0x72, 0xf5, 0xb2, 0x80, 0x1c, 0xa6, 0xcd,
0x1d, 0x38, 0x9f, 0x2b, 0x87, 0x08, 0xba, 0xc2, 0xa4, 0x19, 0x55, 0xa5, 0xfc, 0x74, 0x91, 0x9f,
0x70, 0x76, 0x91, 0x66, 0xc0, 0xd1, 0xa8, 0x5a, 0xab, 0x5b, 0x72, 0x62, 0x54, 0xf5, 0x71, 0xe6,
0x32, 0x85, 0x94, 0x3a, 0x9f, 0xfb, 0x9d, 0xf0, 0x38, 0xc1, 0x97, 0x29, 0x6a, 0x1b, 0x6d, 0x8c,
0xd8, 0x49, 0x0b, 0xe0, 0x26, 0xd2, 0x8f, 0x40, 0xf0, 0x34, 0x29, 0xc7, 0x20, 0x4e, 0x62, 0x1e,
0xcf, 0x41, 0x00, 0xc7, 0x91, 0xae, 0x90, 0x91, 0xc3, 0x10, 0x91, 0x4e, 0xb1, 0xca, 0xe1, 0xef,
0x47, 0xef, 0x56, 0x03, 0x3d, 0xe4, 0xea, 0xcb, 0xf4, 0x2f, 0xe4, 0x9f, 0xb4, 0x18, 0xbe, 0xa7,
0x6d, 0x8c, 0x05, 0x87, 0x78, 0xde, 0xd8, 0x7e, 0x47, 0xff, 0x2e, 0xc1, 0xad, 0x41, 0xd5, 0x20,
0xc7, 0x4c, 0xa4, 0x17, 0xd5, 0xba, 0x4a, 0x9d, 0x62, 0xa1, 0x06, 0xb1, 0xc5, 0xa3, 0xc0, 0x27,
0x03, 0x7c, 0x9c, 0x19, 0x68, 0x6c, 0xe9, 0x29, 0x14, 0x19, 0x1e, 0x68, 0x1c, 0x6d, 0x09, 0x10,
0x03, 0x8d, 0x17, 0x34, 0xd1, 0x65, 0x8b, 0x27, 0x10, 0xae, 0xcc, 0x04, 0xfa, 0x55, 0x66, 0xe2,
0xbc, 0x23, 0x90, 0x45, 0xef, 0x1e, 0xc1, 0xfc, 0x1c, 0x78, 0x79, 0x99, 0x16, 0xfb, 0xd5, 0x0c,
0x1b, 0x8b, 0x05, 0x7e, 0x8b, 0xce, 0x10, 0x23, 0x8d, 0x10, 0x69, 0x08, 0x81, 0x9a, 0xa1, 0xcc,
0x00, 0x07, 0xe5, 0x71, 0x3c, 0x07, 0xf9, 0x01, 0x84, 0xe1, 0x3a, 0x65, 0xc4, 0x82, 0x88, 0xa1,
0x8c, 0x84, 0xad, 0xd7, 0x8d, 0x0c, 0x73, 0x0a, 0xb3, 0x2a, 0xc2, 0xf8, 0x49, 0xbc, 0x9a, 0x43,
0x2e, 0x94, 0x49, 0xb4, 0x09, 0x6b, 0x99, 0xf4, 0xf3, 0xc4, 0x26, 0x6c, 0x1f, 0x3d, 0x2b, 0xe9,
0x76, 0x1e, 0xfc, 0x09, 0xe3, 0xa2, 0xfe, 0xbb, 0x13, 0x67, 0x3c, 0x43, 0x49, 0xb7, 0xfb, 0x50,
0x1d, 0x92, 0x48, 0xba, 0xc3, 0x1a, 0xd6, 0x07, 0x9b, 0x9d, 0x32, 0xbc, 0x02, 0xae, 0xe3, 0xe4,
0xc5, 0x3c, 0x4e, 0x33, 0x15, 0x0d, 0x5f, 0x04, 0x6c, 0x13, 0x3a, 0xc4, 0x07, 0x9b, 0xfb, 0xea,
0x5a, 0x9f, 0xb8, 0x0e, 0x97, 0x10, 0xed, 0x09, 0x77, 0xd8, 0x27, 0xf6, 0x84, 0xbb, 0xb5, 0xcc,
0x52, 0xcd, 0xb0, 0x92, 0x5b, 0x49, 0x62, 0x87, 0x4d, 0xf1, 0x06, 0x91, 0x65, 0x13, 0x81, 0xc4,
0x52, 0x2d, 0xa8, 0x60, 0xe6, 0x36, 0x83, 0xed, 0xa5, 0x79, 0x9c, 0xa5, 0x3f, 0xc5, 0x77, 0x9f,
0x2d, 0x3b, 0x0d, 0x41, 0xcc, 0x6d, 0x7e, 0xd2, 0xe7, 0x6a, 0x1f, 0xc4, 0x24, 0xad, 0x86, 0xfe,
0xb5, 0xc0, 0x73, 0x93, 0x44, 0xb7, 0x2b, 0x8b, 0x54, 0xae, 0x7e, 0x36, 0x88, 0x6e, 0xe3, 0xc7,
0xba, 0x5d, 0x14, 0xe3, 0x2a, 0x25, 0x39, 0x85, 0x04, 0xd2, 0x42, 0x0c, 0x3f, 0x0d, 0x3f, 0x2b,
0x84, 0x13, 0x27, 0xeb, 0x3d, 0xd4, 0xac, 0xf3, 0xda, 0x6a, 0x2c, 0x19, 0xd7, 0x7f, 0x90, 0xe9,
0xac, 0x04, 0xae, 0x66, 0xca, 0x7d, 0x10, 0xa8, 0x77, 0x5a, 0xdc, 0xc8, 0x02, 0xab, 0x8a, 0x12,
0xbd, 0x33, 0xac, 0x61, 0x76, 0x77, 0x2c, 0xee, 0x14, 0x4a, 0x96, 0x2d, 0x41, 0x5e, 0x7f, 0x7b,
0x42, 0x1a, 0xb3, 0x28, 0x62, 0x77, 0x87, 0xa6, 0x4d, 0xba, 0xd1, 0x76, 0xbb, 0x9d, 0xaf, 0x0e,
0xf0, 0x19, 0xb9, 0xc7, 0x92, 0xc4, 0x88, 0x74, 0x23, 0x80, 0x5b, 0xbb, 0x9f, 0x9c, 0xc5, 0xd3,
0x24, 0x2e, 0xc5, 0x49, 0xbc, 0xca, 0x58, 0x3c, 0x95, 0xf3, 0x3a, 0xde, 0xfd, 0x6c, 0x98, 0x91,
0x0d, 0x51, 0xbb, 0x9f, 0x14, 0x5c, 0xfb, 0x7c, 0xfe, 0xe1, 0xff, 0x7c, 0x7d, 0x6b, 0xf0, 0xf3,
0xaf, 0x6f, 0x0d, 0xfe, 0xff, 0xeb, 0x5b, 0x83, 0x9f, 0x7d, 0x73, 0xeb, 0xad, 0x9f, 0x7f, 0x73,
0xeb, 0xad, 0xff, 0xfb, 0xe6, 0xd6, 0x5b, 0x5f, 0xbd, 0xad, 0xfe, 0x16, 0xd7, 0xf9, 0x2f, 0xc9,
0xbf, 0xa8, 0xf5, 0xec, 0x17, 0x01, 0x00, 0x00, 0xff, 0xff, 0x7a, 0x5e, 0x7f, 0x24, 0xaf, 0x6b,
0x00, 0x00,
0x52, 0xc0, 0xb7, 0x5f, 0x58, 0xa8, 0xe3, 0x16, 0xe8, 0x85, 0x65, 0x6f, 0xb9, 0x9b, 0xef, 0x6f,
0x8f, 0xdb, 0xde, 0x99, 0xfd, 0x38, 0xf6, 0x90, 0x90, 0xc7, 0x1e, 0x7b, 0xcd, 0xd9, 0x1e, 0xe3,
0x6e, 0xcf, 0x48, 0x2b, 0x21, 0x51, 0xae, 0x0e, 0xb7, 0x0b, 0x57, 0x57, 0xd6, 0x55, 0x65, 0xb7,
0xa7, 0x0f, 0x81, 0x40, 0x20, 0x10, 0x08, 0xc4, 0x89, 0xaf, 0x17, 0x1e, 0x90, 0xf8, 0x6b, 0xe0,
0xed, 0x1e, 0xef, 0x11, 0xed, 0xfe, 0x23, 0xa7, 0xca, 0xcc, 0xca, 0x8f, 0xa8, 0x8c, 0xac, 0xf2,
0x3e, 0xcd, 0xa8, 0xe3, 0x17, 0x11, 0x99, 0x95, 0x91, 0x99, 0x91, 0x1f, 0x55, 0x8e, 0x6e, 0x16,
0x67, 0x1b, 0x45, 0xc9, 0x38, 0xab, 0x36, 0x2a, 0x28, 0x97, 0x69, 0x02, 0xcd, 0xbf, 0x23, 0xf1,
0xf3, 0xf0, 0xdd, 0x38, 0x5f, 0xf1, 0x55, 0x01, 0x1f, 0x7d, 0x68, 0xc8, 0x84, 0xcd, 0xe7, 0x71,
0x3e, 0xad, 0x24, 0xf2, 0xd1, 0x07, 0x46, 0x02, 0x4b, 0xc8, 0xb9, 0xfa, 0xfd, 0xd9, 0xff, 0xfd,
0x62, 0x10, 0xbd, 0xb7, 0x9d, 0xa5, 0x90, 0xf3, 0x6d, 0xa5, 0x31, 0xfc, 0x2a, 0xfa, 0xee, 0x56,
0x51, 0xec, 0x01, 0x7f, 0x0d, 0x65, 0x95, 0xb2, 0x7c, 0x78, 0x77, 0xa4, 0x1c, 0x8c, 0x4e, 0x8a,
0x64, 0xb4, 0x55, 0x14, 0x23, 0x23, 0x1c, 0x9d, 0xc0, 0x4f, 0x16, 0x50, 0xf1, 0x8f, 0xee, 0x85,
0xa1, 0xaa, 0x60, 0x79, 0x05, 0xc3, 0xf3, 0xe8, 0xb7, 0xb6, 0x8a, 0x62, 0x0c, 0x7c, 0x07, 0xea,
0x0a, 0x8c, 0x79, 0xcc, 0x61, 0xf8, 0xb0, 0xa5, 0xea, 0x02, 0xda, 0xc7, 0xa3, 0x6e, 0x50, 0xf9,
0x99, 0x44, 0xdf, 0xa9, 0xfd, 0x5c, 0x2c, 0xf8, 0x94, 0x5d, 0xe5, 0xc3, 0xdb, 0x6d, 0x45, 0x25,
0xd2, 0xb6, 0xef, 0x84, 0x10, 0x65, 0xf5, 0x4d, 0xf4, 0xeb, 0x6f, 0xe2, 0x2c, 0x03, 0xbe, 0x5d,
0x42, 0x5d, 0x70, 0x57, 0x47, 0x8a, 0x46, 0x52, 0xa6, 0xed, 0xde, 0x0d, 0x32, 0xca, 0xf0, 0x57,
0xd1, 0x77, 0xa5, 0xe4, 0x04, 0x12, 0xb6, 0x84, 0x72, 0xe8, 0xd5, 0x52, 0x42, 0xe2, 0x91, 0xb7,
0x20, 0x6c, 0x7b, 0x9b, 0xe5, 0x4b, 0x28, 0xb9, 0xdf, 0xb6, 0x12, 0x86, 0x6d, 0x1b, 0x48, 0xd9,
0xfe, 0x87, 0x41, 0xf4, 0xfd, 0xad, 0x24, 0x61, 0x8b, 0x9c, 0x1f, 0xb0, 0x24, 0xce, 0x0e, 0xd2,
0xfc, 0xf2, 0x08, 0xae, 0xb6, 0x2f, 0x6a, 0x3e, 0x9f, 0xc1, 0xf0, 0xb9, 0xfb, 0x54, 0x25, 0x3a,
0xd2, 0xec, 0xc8, 0x86, 0xb5, 0xef, 0x4f, 0xae, 0xa7, 0xa4, 0xca, 0xf2, 0x2f, 0x83, 0xe8, 0x06,
0x2e, 0xcb, 0x98, 0x65, 0x4b, 0x30, 0xa5, 0xf9, 0xb4, 0xc3, 0xb0, 0x8b, 0xeb, 0xf2, 0x7c, 0x76,
0x5d, 0x35, 0x55, 0xa2, 0x2c, 0x7a, 0xdf, 0x0e, 0x97, 0x31, 0x54, 0xa2, 0x3b, 0x3d, 0xa6, 0x23,
0x42, 0x21, 0xda, 0xf3, 0x93, 0x3e, 0xa8, 0xf2, 0x96, 0x46, 0x43, 0xe5, 0x2d, 0x63, 0x95, 0x76,
0xf6, 0xc8, 0x6b, 0xc1, 0x22, 0xb4, 0xaf, 0xc7, 0x3d, 0x48, 0xe5, 0xea, 0x4f, 0xa3, 0xdf, 0x78,
0xc3, 0xca, 0xcb, 0xaa, 0x88, 0x13, 0x50, 0x5d, 0xe1, 0xbe, 0xab, 0xdd, 0x48, 0x71, 0x6f, 0x78,
0xd0, 0x85, 0x59, 0x41, 0xdb, 0x08, 0x5f, 0x15, 0x80, 0xc7, 0x20, 0xa3, 0x58, 0x0b, 0xa9, 0xa0,
0xc5, 0x90, 0xb2, 0x7d, 0x19, 0x0d, 0x8d, 0xed, 0xb3, 0x3f, 0x83, 0x84, 0x6f, 0x4d, 0xa7, 0xb8,
0x55, 0x8c, 0xae, 0x20, 0x46, 0x5b, 0xd3, 0x29, 0xd5, 0x2a, 0x7e, 0x54, 0x39, 0xbb, 0x8a, 0x3e,
0x40, 0xce, 0x0e, 0xd2, 0x4a, 0x38, 0x5c, 0x0f, 0x5b, 0x51, 0x98, 0x76, 0x3a, 0xea, 0x8b, 0x2b,
0xc7, 0x7f, 0x35, 0x88, 0xbe, 0xe7, 0xf1, 0x7c, 0x02, 0x73, 0xb6, 0x84, 0xe1, 0x66, 0xb7, 0x35,
0x49, 0x6a, 0xff, 0x1f, 0x5f, 0x43, 0xc3, 0x13, 0x26, 0x63, 0xc8, 0x20, 0xe1, 0x64, 0x98, 0x48,
0x71, 0x67, 0x98, 0x68, 0xcc, 0xea, 0x61, 0x8d, 0x70, 0x0f, 0xf8, 0xf6, 0xa2, 0x2c, 0x21, 0xe7,
0x64, 0x5b, 0x1a, 0xa4, 0xb3, 0x2d, 0x1d, 0xd4, 0x53, 0x9f, 0x3d, 0xe0, 0x5b, 0x59, 0x46, 0xd6,
0x47, 0x8a, 0x3b, 0xeb, 0xa3, 0x31, 0xe5, 0x21, 0x89, 0x7e, 0xd3, 0x7a, 0x62, 0x7c, 0x3f, 0x3f,
0x67, 0x43, 0xfa, 0x59, 0x08, 0xb9, 0xf6, 0xf1, 0xb0, 0x93, 0xf3, 0x54, 0xe3, 0xe5, 0xdb, 0x82,
0x95, 0x74, 0xb3, 0x48, 0x71, 0x67, 0x35, 0x34, 0xa6, 0x3c, 0xfc, 0x49, 0xf4, 0x9e, 0x1a, 0x25,
0x9b, 0xf9, 0xec, 0x9e, 0x77, 0x08, 0xc5, 0x13, 0xda, 0xfd, 0x0e, 0xca, 0x0c, 0x0e, 0x4a, 0xa6,
0x06, 0x9f, 0xbb, 0x5e, 0x3d, 0x34, 0xf4, 0xdc, 0x0b, 0x43, 0x2d, 0xdb, 0x3b, 0x90, 0x01, 0x69,
0x5b, 0x0a, 0x3b, 0x6c, 0x6b, 0x48, 0xd9, 0x2e, 0xa3, 0xdf, 0xd1, 0x8f, 0xa5, 0x9e, 0x47, 0x85,
0xbc, 0x1e, 0xa4, 0xd7, 0x88, 0x7a, 0xdb, 0x90, 0xf6, 0xf5, 0xb4, 0x1f, 0xdc, 0xaa, 0x8f, 0xea,
0x81, 0xfe, 0xfa, 0xa0, 0xfe, 0x77, 0x2f, 0x0c, 0x29, 0xdb, 0xff, 0x38, 0x88, 0x7e, 0xa0, 0x64,
0x2f, 0xf3, 0xf8, 0x2c, 0x03, 0x31, 0x25, 0x1e, 0x01, 0xbf, 0x62, 0xe5, 0xe5, 0x78, 0x95, 0x27,
0xc4, 0xf4, 0xef, 0x87, 0x3b, 0xa6, 0x7f, 0x52, 0xc9, 0xca, 0xf8, 0x54, 0x45, 0x39, 0x2b, 0x70,
0xc6, 0xd7, 0xd4, 0x80, 0xb3, 0x82, 0xca, 0xf8, 0x5c, 0xa4, 0x65, 0xf5, 0xb0, 0x1e, 0x36, 0xfd,
0x56, 0x0f, 0xed, 0x71, 0xf2, 0x4e, 0x08, 0x31, 0xc3, 0x56, 0x13, 0xc0, 0x2c, 0x3f, 0x4f, 0x67,
0xa7, 0xc5, 0xb4, 0x0e, 0xe3, 0xc7, 0xfe, 0x08, 0xb5, 0x10, 0x62, 0xd8, 0x22, 0x50, 0xe5, 0xed,
0x9f, 0x4d, 0x62, 0xa4, 0xba, 0xd2, 0x6e, 0xc9, 0xe6, 0x07, 0x30, 0x8b, 0x93, 0x95, 0xea, 0xff,
0x9f, 0x84, 0x3a, 0x1e, 0xa6, 0x75, 0x21, 0x3e, 0xbd, 0xa6, 0x96, 0x2a, 0xcf, 0x7f, 0x0f, 0xa2,
0x7b, 0x4d, 0xf5, 0x2f, 0xe2, 0x7c, 0x06, 0xaa, 0x3d, 0x65, 0xe9, 0xb7, 0xf2, 0xe9, 0x09, 0x54,
0x3c, 0x2e, 0xf9, 0xf0, 0x0b, 0x7f, 0x25, 0x43, 0x3a, 0xba, 0x6c, 0x3f, 0xfa, 0x56, 0xba, 0xa6,
0xd5, 0xc7, 0xf5, 0xc0, 0xa6, 0x86, 0x00, 0xb7, 0xd5, 0x85, 0x04, 0x0f, 0x00, 0x77, 0x42, 0x88,
0x69, 0x75, 0x21, 0xd8, 0xcf, 0x97, 0x29, 0x87, 0x3d, 0xc8, 0xa1, 0x6c, 0xb7, 0xba, 0x54, 0x75,
0x11, 0xa2, 0xd5, 0x09, 0xd4, 0x0c, 0x36, 0x8e, 0x37, 0x3d, 0x39, 0xae, 0x05, 0x8c, 0xb4, 0xa6,
0xc7, 0xa7, 0xfd, 0x60, 0xb3, 0xba, 0xb3, 0x7c, 0x9e, 0xc0, 0x92, 0x5d, 0xe2, 0xd5, 0x9d, 0x6d,
0x42, 0x02, 0xc4, 0xea, 0xce, 0x0b, 0x9a, 0x19, 0xcc, 0xf2, 0xf3, 0x3a, 0x85, 0x2b, 0x34, 0x83,
0xd9, 0xca, 0xb5, 0x98, 0x98, 0xc1, 0x3c, 0x98, 0xf2, 0x70, 0x14, 0xfd, 0x9a, 0x10, 0xfe, 0x11,
0x4b, 0xf3, 0xe1, 0x4d, 0x8f, 0x52, 0x2d, 0xd0, 0x56, 0x6f, 0xd1, 0x00, 0x2a, 0x71, 0xfd, 0xeb,
0x76, 0x9c, 0x27, 0x90, 0x79, 0x4b, 0x6c, 0xc4, 0xc1, 0x12, 0x3b, 0x98, 0x49, 0x1d, 0x84, 0xb0,
0x1e, 0xbf, 0xc6, 0x17, 0x71, 0x99, 0xe6, 0xb3, 0xa1, 0x4f, 0xd7, 0x92, 0x13, 0xa9, 0x83, 0x8f,
0x43, 0x21, 0xac, 0x14, 0xb7, 0x8a, 0xa2, 0xac, 0x87, 0x45, 0x5f, 0x08, 0xbb, 0x48, 0x30, 0x84,
0x5b, 0xa8, 0xdf, 0xdb, 0x0e, 0x24, 0x59, 0x9a, 0x07, 0xbd, 0x29, 0xa4, 0x8f, 0x37, 0x83, 0xa2,
0xe0, 0x3d, 0x80, 0x78, 0x09, 0x4d, 0xcd, 0x7c, 0x4f, 0xc6, 0x06, 0x82, 0xc1, 0x8b, 0x40, 0xb3,
0x4e, 0x13, 0xe2, 0xc3, 0xf8, 0x12, 0xea, 0x07, 0x0c, 0xf5, 0xbc, 0x36, 0xf4, 0xe9, 0x3b, 0x04,
0xb1, 0x4e, 0xf3, 0x93, 0xca, 0xd5, 0x22, 0xfa, 0x40, 0xc8, 0x8f, 0xe3, 0x92, 0xa7, 0x49, 0x5a,
0xc4, 0x79, 0x93, 0xff, 0xfb, 0xfa, 0x75, 0x8b, 0xd2, 0x2e, 0xd7, 0x7b, 0xd2, 0xca, 0xed, 0x7f,
0x0e, 0xa2, 0xdb, 0xd8, 0xef, 0x31, 0x94, 0xf3, 0x54, 0x2c, 0x23, 0x2b, 0x39, 0x08, 0x0f, 0x3f,
0x0f, 0x1b, 0x6d, 0x29, 0xe8, 0xd2, 0xfc, 0xf0, 0xfa, 0x8a, 0xaa, 0x60, 0x7f, 0x1c, 0x45, 0x72,
0xb9, 0x22, 0x96, 0x94, 0x6e, 0xaf, 0x55, 0xeb, 0x18, 0x67, 0x3d, 0x79, 0x3b, 0x40, 0x98, 0xa9,
0x42, 0xfe, 0x2e, 0x56, 0xca, 0x43, 0xaf, 0x86, 0x10, 0x11, 0x53, 0x05, 0x42, 0x70, 0x41, 0xc7,
0x17, 0xec, 0xca, 0x5f, 0xd0, 0x5a, 0x12, 0x2e, 0xa8, 0x22, 0xcc, 0xde, 0x95, 0x2a, 0xa8, 0x6f,
0xef, 0xaa, 0x29, 0x46, 0x68, 0xef, 0x0a, 0x33, 0xca, 0x30, 0x8b, 0x7e, 0xdb, 0x36, 0xfc, 0x82,
0xb1, 0xcb, 0x79, 0x5c, 0x5e, 0x0e, 0x9f, 0xd0, 0xca, 0x0d, 0xa3, 0x1d, 0xad, 0xf5, 0x62, 0xcd,
0xb0, 0x60, 0x3b, 0xac, 0x13, 0x8d, 0xd3, 0x32, 0x43, 0xc3, 0x82, 0x63, 0x43, 0x21, 0xc4, 0xb0,
0x40, 0xa0, 0x66, 0xe4, 0xb6, 0xbd, 0x8d, 0x01, 0xaf, 0x96, 0x1c, 0xf5, 0x31, 0x50, 0xab, 0x25,
0x0f, 0x86, 0x43, 0x68, 0xaf, 0x8c, 0x8b, 0x0b, 0x7f, 0x08, 0x09, 0x51, 0x38, 0x84, 0x1a, 0x04,
0xb7, 0xf7, 0x18, 0xe2, 0x32, 0xb9, 0xf0, 0xb7, 0xb7, 0x94, 0x85, 0xdb, 0x5b, 0x33, 0xb8, 0xbd,
0xa5, 0xe0, 0x4d, 0xca, 0x2f, 0x0e, 0x81, 0xc7, 0xfe, 0xf6, 0x76, 0x99, 0x70, 0x7b, 0xb7, 0x58,
0x93, 0xc9, 0xd8, 0x0e, 0xc7, 0x8b, 0xb3, 0x2a, 0x29, 0xd3, 0x33, 0x18, 0x06, 0xac, 0x68, 0x88,
0xc8, 0x64, 0x48, 0xd8, 0x0c, 0xd2, 0xca, 0x67, 0x23, 0xdb, 0x9f, 0x56, 0x68, 0x90, 0x6e, 0x6c,
0x58, 0x04, 0x31, 0x48, 0xfb, 0x49, 0x5c, 0xbd, 0xbd, 0x92, 0x2d, 0x8a, 0xaa, 0xa3, 0x7a, 0x08,
0x0a, 0x57, 0xaf, 0x0d, 0x2b, 0x9f, 0x6f, 0xa3, 0xdf, 0xb5, 0x1f, 0xe9, 0x69, 0x5e, 0x69, 0xaf,
0xeb, 0xf4, 0x73, 0xb2, 0x30, 0x62, 0x5b, 0x2a, 0x80, 0x9b, 0x34, 0xa5, 0xf1, 0xcc, 0x77, 0x80,
0xc7, 0x69, 0x56, 0x0d, 0x1f, 0xf8, 0x6d, 0x34, 0x72, 0x22, 0x4d, 0xf1, 0x71, 0xb8, 0xcf, 0xee,
0x2c, 0x8a, 0x2c, 0x4d, 0xda, 0xfb, 0x93, 0x4a, 0x57, 0x8b, 0xc3, 0x7d, 0xd6, 0xc6, 0xf0, 0x18,
0x34, 0x06, 0x2e, 0xff, 0x33, 0x59, 0x15, 0xe0, 0x1f, 0x83, 0x1c, 0x24, 0x3c, 0x06, 0x61, 0x14,
0xd7, 0x67, 0x0c, 0xfc, 0x20, 0x5e, 0xb1, 0x05, 0x31, 0x06, 0x69, 0x71, 0xb8, 0x3e, 0x36, 0x66,
0x32, 0x05, 0xed, 0x61, 0x3f, 0xe7, 0x50, 0xe6, 0x71, 0xb6, 0x9b, 0xc5, 0xb3, 0x6a, 0x48, 0xf4,
0x1b, 0x97, 0x22, 0x32, 0x05, 0x9a, 0xf6, 0x3c, 0xc6, 0xfd, 0x6a, 0x37, 0x5e, 0xb2, 0x32, 0xe5,
0xf4, 0x63, 0x34, 0x48, 0xe7, 0x63, 0x74, 0x50, 0xaf, 0xb7, 0xad, 0x32, 0xb9, 0x48, 0x97, 0x30,
0x0d, 0x78, 0x6b, 0x90, 0x1e, 0xde, 0x2c, 0xd4, 0xd3, 0x68, 0x63, 0xb6, 0x28, 0x13, 0x20, 0x1b,
0x4d, 0x8a, 0x3b, 0x1b, 0x4d, 0x63, 0xca, 0xc3, 0xdf, 0x0e, 0xa2, 0xdf, 0x93, 0x52, 0x7b, 0xd3,
0x70, 0x27, 0xae, 0x2e, 0xce, 0x58, 0x5c, 0x4e, 0x87, 0x1f, 0xfb, 0xec, 0x78, 0x51, 0xed, 0xfa,
0xd9, 0x75, 0x54, 0xf0, 0x63, 0x3d, 0x48, 0x2b, 0xab, 0xc7, 0x79, 0x1f, 0xab, 0x83, 0x84, 0x1f,
0x2b, 0x46, 0xf1, 0x00, 0x22, 0xe4, 0x72, 0x81, 0xfe, 0x80, 0xd4, 0x77, 0x57, 0xe9, 0x0f, 0x3b,
0x39, 0x3c, 0x3e, 0xd6, 0x42, 0x37, 0x5a, 0xd6, 0x29, 0x1b, 0xfe, 0x88, 0x19, 0xf5, 0xc5, 0x49,
0xcf, 0xba, 0x57, 0x84, 0x3d, 0xb7, 0x7a, 0xc6, 0xa8, 0x2f, 0x4e, 0x78, 0xb6, 0x86, 0xb5, 0x90,
0x67, 0xcf, 0xd0, 0x36, 0xea, 0x8b, 0xe3, 0x8c, 0x42, 0x31, 0xcd, 0xbc, 0xf0, 0x24, 0x60, 0x07,
0xcf, 0x0d, 0x6b, 0xbd, 0x58, 0x1c, 0xb1, 0x5b, 0x45, 0x91, 0xad, 0x26, 0x30, 0x2f, 0x32, 0x32,
0x62, 0x1d, 0x24, 0x1c, 0xb1, 0x18, 0xc5, 0xf9, 0xdd, 0x84, 0xd5, 0xd9, 0xa3, 0x37, 0xbf, 0x13,
0xa2, 0x70, 0x7e, 0xd7, 0x20, 0x38, 0x43, 0x99, 0xb0, 0x6d, 0x96, 0x65, 0x90, 0xf0, 0xf6, 0x71,
0x9f, 0xd6, 0x34, 0x44, 0x38, 0x43, 0x41, 0xa4, 0x59, 0x19, 0x37, 0xab, 0x91, 0xb8, 0x84, 0x17,
0xab, 0x83, 0x34, 0xbf, 0x1c, 0xfa, 0x27, 0x63, 0x03, 0x10, 0x2b, 0x63, 0x2f, 0x88, 0x57, 0x3d,
0xa7, 0xf9, 0x94, 0xf9, 0x57, 0x3d, 0xb5, 0x24, 0xbc, 0xea, 0x51, 0x04, 0x36, 0x79, 0x02, 0x94,
0xc9, 0x5a, 0x12, 0x36, 0xa9, 0x08, 0xdf, 0x00, 0xa4, 0xf6, 0x4f, 0xc9, 0x01, 0x08, 0xed, 0x98,
0x3e, 0xec, 0xe4, 0x70, 0x84, 0x36, 0xcb, 0x9f, 0x5d, 0xe0, 0xc9, 0x85, 0x3f, 0x42, 0x1d, 0x24,
0x1c, 0xa1, 0x18, 0xc5, 0x55, 0x9a, 0x30, 0xbd, 0x7c, 0x7b, 0xe0, 0x8f, 0x8f, 0xd6, 0xd2, 0xed,
0x61, 0x27, 0x87, 0x17, 0x24, 0xfb, 0x73, 0xf1, 0xcc, 0xbc, 0x41, 0x2e, 0x65, 0xe1, 0x05, 0x89,
0x66, 0x70, 0xe9, 0xa5, 0xa0, 0x7e, 0x9c, 0xfe, 0xd2, 0x1b, 0x79, 0xb8, 0xf4, 0x0e, 0xa7, 0x9c,
0xfc, 0xfb, 0x20, 0xba, 0x69, 0x7b, 0x39, 0x62, 0x75, 0x1f, 0x79, 0x1d, 0x67, 0xe9, 0x34, 0xe6,
0x30, 0x61, 0x97, 0x90, 0xa3, 0x1d, 0x0d, 0xb7, 0xb4, 0x92, 0x1f, 0x39, 0x0a, 0xc4, 0x8e, 0x46,
0x2f, 0x45, 0x1c, 0x27, 0x92, 0x3e, 0xad, 0x60, 0x3b, 0xae, 0x88, 0x91, 0xcc, 0x41, 0xc2, 0x71,
0x82, 0x51, 0x9c, 0x25, 0x4a, 0xf9, 0xcb, 0xb7, 0x05, 0x94, 0x29, 0xe4, 0x09, 0xf8, 0xb3, 0x44,
0x4c, 0x85, 0xb3, 0x44, 0x0f, 0xdd, 0x5a, 0xf0, 0xeb, 0xc1, 0xa9, 0x7d, 0x62, 0x8f, 0x89, 0xc0,
0x89, 0x3d, 0x81, 0xe2, 0x4a, 0x1a, 0xc0, 0xbb, 0x69, 0xd6, 0xb2, 0x12, 0xdc, 0x34, 0xa3, 0xe9,
0xd6, 0x36, 0x8a, 0x66, 0xc6, 0x75, 0x37, 0xe9, 0x28, 0xfa, 0xd8, 0xee, 0x2e, 0x6b, 0xbd, 0x58,
0xff, 0xbe, 0xcd, 0x09, 0x64, 0xb1, 0x98, 0x42, 0x02, 0x9b, 0x23, 0x0d, 0xd3, 0x67, 0xdf, 0xc6,
0x62, 0x95, 0xc3, 0xbf, 0x1e, 0x44, 0x1f, 0xf9, 0x3c, 0xbe, 0x2a, 0x84, 0xdf, 0xcd, 0x6e, 0x5b,
0x92, 0x24, 0xae, 0x24, 0x84, 0x35, 0x54, 0x19, 0xfe, 0x3c, 0xfa, 0xb0, 0x11, 0x99, 0x1b, 0x0b,
0xaa, 0x00, 0x6e, 0xda, 0xa2, 0xcb, 0x8f, 0x39, 0xed, 0x7e, 0xa3, 0x37, 0x6f, 0x56, 0x04, 0x6e,
0xb9, 0x2a, 0xb4, 0x22, 0xd0, 0x36, 0x94, 0x98, 0x58, 0x11, 0x78, 0x30, 0x3c, 0x53, 0x37, 0x48,
0xdd, 0x4f, 0x7c, 0x63, 0x9c, 0x36, 0x61, 0xf7, 0x92, 0x47, 0xdd, 0x20, 0x8e, 0x9d, 0x46, 0xac,
0x12, 0xf1, 0x27, 0x21, 0x0b, 0x28, 0x19, 0x5f, 0xeb, 0xc5, 0x2a, 0x87, 0x7f, 0x19, 0x7d, 0xaf,
0x55, 0xb1, 0x5d, 0x88, 0xf9, 0xa2, 0x84, 0xe9, 0x70, 0xa3, 0xa3, 0xdc, 0x0d, 0xa8, 0x5d, 0x6f,
0xf6, 0x57, 0x50, 0xfe, 0xff, 0x7e, 0x10, 0x7d, 0xdf, 0xe5, 0x64, 0x13, 0xeb, 0x32, 0x3c, 0x0b,
0x99, 0x74, 0x59, 0x5d, 0x8c, 0xe7, 0xd7, 0xd2, 0x69, 0x2d, 0xfa, 0xec, 0x40, 0xde, 0x5a, 0xc6,
0x69, 0x26, 0x0e, 0x12, 0x3e, 0x0e, 0x19, 0x75, 0xd0, 0xe0, 0xa2, 0x8f, 0x54, 0x69, 0x8d, 0x92,
0xa2, 0xbf, 0x59, 0x8b, 0x85, 0xa7, 0x74, 0xaf, 0xf4, 0xac, 0x15, 0xd6, 0x7b, 0xd2, 0xca, 0x2d,
0x6f, 0x36, 0xcb, 0xea, 0x9f, 0xed, 0x20, 0xf7, 0x79, 0x55, 0xaa, 0x9e, 0x48, 0x5f, 0xef, 0x49,
0x2b, 0xaf, 0x7f, 0x11, 0x7d, 0xd8, 0xf6, 0xaa, 0x26, 0x85, 0x8d, 0x4e, 0x53, 0x68, 0x5e, 0xd8,
0xec, 0xaf, 0x60, 0x52, 0xfd, 0x2f, 0xd3, 0x8a, 0xb3, 0x72, 0x35, 0xbe, 0x60, 0x57, 0xcd, 0xad,
0x5c, 0xb7, 0xb7, 0x2a, 0x60, 0x64, 0x11, 0x44, 0xaa, 0xef, 0x27, 0x5b, 0xae, 0xcc, 0xed, 0xdd,
0x8a, 0x70, 0x65, 0x11, 0x1d, 0xae, 0x5c, 0xd2, 0x8c, 0x55, 0x4d, 0xad, 0xcc, 0x55, 0xe3, 0x87,
0xfe, 0xa2, 0xb6, 0xaf, 0x1b, 0x3f, 0xea, 0x06, 0x4d, 0xf6, 0xa0, 0xc4, 0x3b, 0xe9, 0xf9, 0xb9,
0xae, 0x93, 0xbf, 0xa4, 0x36, 0x42, 0x64, 0x0f, 0x04, 0x6a, 0x92, 0xd1, 0xdd, 0x34, 0x03, 0x71,
0x2a, 0xf5, 0xea, 0xfc, 0x3c, 0x63, 0xf1, 0x14, 0x25, 0xa3, 0xb5, 0x78, 0x64, 0xcb, 0x89, 0x64,
0xd4, 0xc7, 0x99, 0x4b, 0x3d, 0xb5, 0xf4, 0x04, 0x12, 0x96, 0x27, 0x69, 0x86, 0x2f, 0x29, 0x09,
0x4d, 0x2d, 0x24, 0x2e, 0xf5, 0xb4, 0x20, 0x33, 0x49, 0xd5, 0xa2, 0xba, 0xdb, 0x37, 0xe5, 0xbf,
0xdf, 0x56, 0xb4, 0xc4, 0xc4, 0x24, 0xe5, 0xc1, 0xcc, 0x9a, 0xac, 0x16, 0x9e, 0x16, 0xc2, 0xf8,
0xad, 0xb6, 0x96, 0x94, 0x10, 0x6b, 0x32, 0x97, 0x30, 0x6b, 0x8b, 0xfa, 0xf7, 0x1d, 0x76, 0x95,
0x0b, 0xa3, 0x77, 0xda, 0x2a, 0x8d, 0x8c, 0x58, 0x5b, 0x60, 0x46, 0x19, 0xfe, 0x71, 0xf4, 0xab,
0xc2, 0x70, 0xc9, 0x8a, 0xe1, 0x0d, 0x8f, 0x42, 0x69, 0xdd, 0x27, 0xba, 0x49, 0xca, 0xcd, 0xb5,
0x38, 0x1d, 0x1b, 0xa7, 0x55, 0x3c, 0x83, 0xe1, 0x3d, 0xa2, 0xc5, 0x85, 0x94, 0xb8, 0x16, 0xd7,
0xa6, 0xdc, 0xa8, 0x38, 0x62, 0x53, 0x65, 0xdd, 0x53, 0x43, 0x2d, 0x0c, 0x45, 0x85, 0x0d, 0x99,
0x43, 0x8a, 0xa3, 0x78, 0x99, 0xce, 0xf4, 0x84, 0x23, 0xc7, 0xad, 0x0a, 0x1d, 0x52, 0x18, 0x66,
0x64, 0x41, 0xc4, 0x21, 0x05, 0x09, 0x2b, 0x9f, 0xff, 0x36, 0x88, 0x6e, 0x19, 0x66, 0xaf, 0xd9,
0x3b, 0xda, 0xcf, 0xcf, 0xd9, 0x9b, 0x94, 0x5f, 0x1c, 0xa4, 0xf9, 0x65, 0x35, 0xfc, 0x8c, 0x32,
0xe9, 0xe7, 0x75, 0x51, 0x3e, 0xbf, 0xb6, 0x9e, 0xc9, 0x20, 0x9b, 0x2d, 0x1e, 0x73, 0x62, 0x28,
0x35, 0x50, 0x06, 0xa9, 0x77, 0x82, 0x30, 0x47, 0x64, 0x90, 0x21, 0xde, 0x34, 0xb1, 0x76, 0x9e,
0xb1, 0x1c, 0x37, 0xb1, 0xb1, 0x50, 0x0b, 0x89, 0x26, 0x6e, 0x41, 0x66, 0x3c, 0x6e, 0x44, 0x72,
0x37, 0x62, 0x2b, 0xcb, 0xd0, 0x78, 0xac, 0x55, 0x35, 0x40, 0x8c, 0xc7, 0x5e, 0x50, 0xf9, 0x39,
0x89, 0xbe, 0x53, 0x3f, 0xd2, 0xe3, 0x12, 0x96, 0x29, 0xe0, 0xc3, 0x6d, 0x4b, 0x42, 0xf4, 0x7f,
0x97, 0x30, 0x3d, 0xeb, 0x34, 0xaf, 0x8a, 0x2c, 0xae, 0x2e, 0xd4, 0x71, 0xa7, 0x5b, 0xe7, 0x46,
0x88, 0x0f, 0x3c, 0xef, 0x77, 0x50, 0x66, 0x50, 0x6f, 0x64, 0x7a, 0x88, 0x79, 0xe0, 0x57, 0x6d,
0x0d, 0x33, 0x0f, 0x3b, 0x39, 0xb3, 0xff, 0xba, 0x17, 0x67, 0x19, 0x94, 0xab, 0x46, 0x76, 0x18,
0xe7, 0xe9, 0x39, 0x54, 0x1c, 0xed, 0xbf, 0x2a, 0x6a, 0x84, 0x31, 0x62, 0xff, 0x35, 0x80, 0x9b,
0x6c, 0x1e, 0x79, 0xde, 0xcf, 0xa7, 0xf0, 0x16, 0x65, 0xf3, 0xd8, 0x8e, 0x60, 0x88, 0x6c, 0x9e,
0x62, 0xcd, 0x8e, 0xe8, 0x8b, 0x8c, 0x25, 0x97, 0x6a, 0x0a, 0x70, 0x1b, 0x58, 0x48, 0xf0, 0x1c,
0x70, 0x27, 0x84, 0x98, 0x49, 0x40, 0x08, 0x4e, 0xa0, 0xc8, 0xe2, 0x04, 0xdf, 0x70, 0x90, 0x3a,
0x4a, 0x46, 0x4c, 0x02, 0x98, 0x41, 0xc5, 0x55, 0x37, 0x27, 0x7c, 0xc5, 0x45, 0x17, 0x27, 0xee,
0x84, 0x10, 0x33, 0x0d, 0x0a, 0xc1, 0xb8, 0xc8, 0x52, 0x8e, 0xba, 0x81, 0xd4, 0x10, 0x12, 0xa2,
0x1b, 0xb8, 0x04, 0x32, 0x79, 0x08, 0xe5, 0x0c, 0xbc, 0x26, 0x85, 0x24, 0x68, 0xb2, 0x21, 0xcc,
0x45, 0x38, 0x59, 0x77, 0x56, 0xac, 0xd0, 0x45, 0x38, 0x55, 0x2d, 0x56, 0xac, 0x88, 0x8b, 0x70,
0x0e, 0x80, 0x8a, 0x78, 0x1c, 0x57, 0xdc, 0x5f, 0x44, 0x21, 0x09, 0x16, 0xb1, 0x21, 0xcc, 0x1c,
0x2d, 0x8b, 0xb8, 0xe0, 0x68, 0x8e, 0x56, 0x05, 0xb0, 0xce, 0x43, 0x6f, 0x92, 0x72, 0x33, 0x92,
0xc8, 0x56, 0x01, 0xbe, 0x9b, 0x42, 0x36, 0xad, 0xd0, 0x48, 0xa2, 0x9e, 0x7b, 0x23, 0x25, 0x46,
0x92, 0x36, 0x85, 0x42, 0x49, 0xed, 0x1b, 0xfb, 0x6a, 0x87, 0xb6, 0x8c, 0xef, 0x84, 0x10, 0x33,
0x3e, 0x35, 0x85, 0xde, 0x8e, 0xcb, 0x32, 0xad, 0x27, 0xff, 0x07, 0xfe, 0x02, 0x35, 0x72, 0x62,
0x7c, 0xf2, 0x71, 0xa8, 0x7b, 0x35, 0x03, 0xb7, 0xaf, 0x60, 0x78, 0xe8, 0xbe, 0x1b, 0x64, 0x4c,
0xc6, 0x29, 0x24, 0xd6, 0x81, 0x9e, 0xef, 0x69, 0x7a, 0xce, 0xf3, 0x1e, 0x74, 0x61, 0xd6, 0x45,
0x75, 0xed, 0xe2, 0x90, 0x2d, 0x61, 0xc2, 0x5e, 0xbe, 0x4d, 0x2b, 0x9e, 0xe6, 0x33, 0x35, 0x73,
0x3f, 0x27, 0x2c, 0xf9, 0x60, 0xe2, 0xa2, 0x7a, 0xa7, 0x92, 0x49, 0x20, 0x50, 0x59, 0x8e, 0xe0,
0xca, 0x9b, 0x40, 0x60, 0x8b, 0x9a, 0x23, 0x12, 0x88, 0x10, 0x6f, 0xf6, 0x51, 0xb4, 0x73, 0xf5,
0x36, 0xdf, 0x84, 0x35, 0xb9, 0x1c, 0x65, 0x0d, 0x83, 0xc4, 0x52, 0x36, 0xa8, 0x60, 0xd6, 0x97,
0xda, 0xbf, 0xe9, 0x62, 0x8f, 0x08, 0x3b, 0xed, 0x6e, 0xf6, 0xb8, 0x07, 0xe9, 0x71, 0x65, 0x4e,
0xa5, 0x29, 0x57, 0xed, 0x43, 0xe9, 0xc7, 0x3d, 0x48, 0x6b, 0x4f, 0xc6, 0xae, 0xd6, 0x8b, 0x38,
0xb9, 0x9c, 0x95, 0x6c, 0x91, 0x4f, 0xb7, 0x59, 0xc6, 0x4a, 0xb4, 0x27, 0xe3, 0x94, 0x1a, 0xa1,
0xc4, 0x9e, 0x4c, 0x87, 0x8a, 0xc9, 0xe0, 0xec, 0x52, 0x6c, 0x65, 0xe9, 0x0c, 0xaf, 0xa8, 0x1d,
0x43, 0x02, 0x20, 0x32, 0x38, 0x2f, 0xe8, 0x09, 0x22, 0xb9, 0xe2, 0xe6, 0x69, 0x12, 0x67, 0xd2,
0xdf, 0x06, 0x6d, 0xc6, 0x01, 0x3b, 0x83, 0xc8, 0xa3, 0xe0, 0xa9, 0xe7, 0x64, 0x51, 0xe6, 0xfb,
0x39, 0x67, 0x64, 0x3d, 0x1b, 0xa0, 0xb3, 0x9e, 0x16, 0x88, 0x86, 0xd5, 0x09, 0xbc, 0xad, 0x4b,
0x53, 0xff, 0xe3, 0x1b, 0x56, 0xeb, 0xdf, 0x47, 0x4a, 0x1e, 0x1a, 0x56, 0x11, 0x87, 0x2a, 0xa3,
0x9c, 0xc8, 0x80, 0x09, 0x68, 0xbb, 0x61, 0xf2, 0xa8, 0x1b, 0xf4, 0xfb, 0x19, 0xf3, 0x55, 0x06,
0x21, 0x3f, 0x02, 0xe8, 0xe3, 0xa7, 0x01, 0xcd, 0x76, 0x8b, 0x53, 0x9f, 0x0b, 0x48, 0x2e, 0x5b,
0x97, 0x6c, 0xdc, 0x82, 0x4a, 0x84, 0xd8, 0x6e, 0x21, 0x50, 0x7f, 0x13, 0xed, 0x27, 0x2c, 0x0f,
0x35, 0x51, 0x2d, 0xef, 0xd3, 0x44, 0x8a, 0x33, 0x8b, 0x5f, 0x2d, 0x55, 0x91, 0x29, 0x9b, 0x69,
0x8d, 0xb0, 0x60, 0x43, 0xc4, 0xe2, 0x97, 0x84, 0x4d, 0x4e, 0x8e, 0x7d, 0x1e, 0xb6, 0x6f, 0xd5,
0xb6, 0xac, 0x1c, 0xd2, 0xb7, 0x6a, 0x29, 0x96, 0xae, 0xa4, 0x8c, 0x91, 0x0e, 0x2b, 0x6e, 0x9c,
0x3c, 0xed, 0x07, 0x9b, 0x25, 0x8f, 0xe3, 0x73, 0x3b, 0x83, 0xb8, 0x94, 0x5e, 0xd7, 0x03, 0x86,
0x0c, 0x46, 0x2c, 0x79, 0x02, 0x38, 0x1a, 0xc2, 0x1c, 0xcf, 0xdb, 0x2c, 0xe7, 0x90, 0x73, 0xdf,
0x10, 0xe6, 0x1a, 0x53, 0x60, 0x68, 0x08, 0xa3, 0x14, 0x50, 0xdc, 0x8a, 0xfd, 0x20, 0xe0, 0x47,
0xf1, 0xdc, 0x9b, 0xb1, 0xc9, 0xbd, 0x1e, 0x29, 0x0f, 0xc5, 0x2d, 0xe2, 0xac, 0x03, 0x37, 0xdb,
0xcb, 0x24, 0x2e, 0x67, 0x7a, 0x77, 0x63, 0x3a, 0xdc, 0xa4, 0xed, 0xb8, 0x24, 0x71, 0xe0, 0x16,
0xd6, 0x40, 0xc3, 0xce, 0xfe, 0x3c, 0x9e, 0xe9, 0x9a, 0x7a, 0x6a, 0x20, 0xe4, 0xad, 0xaa, 0x3e,
0xea, 0x06, 0x91, 0x9f, 0xd7, 0xe9, 0x14, 0x58, 0xc0, 0x8f, 0x90, 0xf7, 0xf1, 0x83, 0x41, 0x94,
0xbd, 0xd5, 0xf5, 0x96, 0x2b, 0xba, 0xad, 0x7c, 0xaa, 0xd6, 0xb1, 0x23, 0xe2, 0xf1, 0x20, 0x2e,
0x94, 0xbd, 0x11, 0x3c, 0xea, 0xa3, 0xcd, 0x06, 0x6d, 0xa8, 0x8f, 0xea, 0xfd, 0xd7, 0x3e, 0x7d,
0xd4, 0x07, 0x2b, 0x9f, 0x3f, 0x55, 0x7d, 0x74, 0x27, 0xe6, 0x71, 0x9d, 0xb7, 0xbf, 0x4e, 0xe1,
0x4a, 0x2d, 0x84, 0x3d, 0xf5, 0x6d, 0xa8, 0x91, 0x78, 0x9d, 0x0a, 0xad, 0x8a, 0x37, 0x7a, 0xf3,
0x01, 0xdf, 0x6a, 0x85, 0xd0, 0xe9, 0x1b, 0x2d, 0x15, 0x36, 0x7a, 0xf3, 0x01, 0xdf, 0xea, 0x3d,
0xcd, 0x4e, 0xdf, 0xe8, 0x65, 0xcd, 0x8d, 0xde, 0xbc, 0xf2, 0xfd, 0x37, 0x4d, 0xc7, 0xb5, 0x9d,
0xd7, 0x79, 0x58, 0xc2, 0xd3, 0x25, 0xf8, 0xd2, 0x49, 0xd7, 0x9e, 0x46, 0x43, 0xe9, 0x24, 0xad,
0x62, 0x7d, 0xdc, 0xc3, 0x57, 0x8a, 0x63, 0x56, 0xa5, 0xe2, 0xc0, 0xfc, 0x79, 0x0f, 0xa3, 0x0d,
0x1c, 0x5a, 0x34, 0x85, 0x94, 0xcc, 0x71, 0xa3, 0x83, 0x9a, 0x3b, 0xb5, 0x4f, 0x03, 0xf6, 0xda,
0x57, 0x6b, 0xd7, 0x7b, 0xd2, 0xe6, 0xe0, 0xcf, 0x61, 0xec, 0x13, 0xc7, 0x50, 0xab, 0x7a, 0x0f,
0x1d, 0x37, 0xfb, 0x2b, 0x28, 0xf7, 0x7f, 0xd7, 0xac, 0x2b, 0xb0, 0x7f, 0xd5, 0x09, 0x9e, 0xf5,
0xb1, 0x88, 0x3a, 0xc2, 0xf3, 0x6b, 0xe9, 0xa8, 0x82, 0xfc, 0xd7, 0x20, 0xba, 0xe3, 0x2d, 0x88,
0x7b, 0xf6, 0xfc, 0xfb, 0x7d, 0x6c, 0xfb, 0xcf, 0xa0, 0xbf, 0xf8, 0x36, 0xaa, 0xaa, 0x74, 0xff,
0xd4, 0x2c, 0xef, 0x1b, 0x0d, 0xf1, 0xde, 0xc3, 0xab, 0x72, 0x0a, 0xa5, 0xea, 0xb1, 0xa1, 0xa0,
0x33, 0x30, 0xee, 0xb7, 0x9f, 0x5e, 0x53, 0xcb, 0xfa, 0x10, 0x8d, 0x03, 0xab, 0x77, 0xce, 0xac,
0xf2, 0x84, 0x2c, 0x5b, 0x34, 0x2e, 0xd0, 0x67, 0xd7, 0x55, 0xa3, 0x7a, 0xb2, 0x05, 0x8b, 0xf7,
0xda, 0x9f, 0xf7, 0x34, 0xec, 0xbc, 0xe9, 0xfe, 0xc9, 0xf5, 0x94, 0x54, 0x59, 0xfe, 0x67, 0x10,
0xdd, 0x77, 0x58, 0x73, 0xda, 0x81, 0xf6, 0x64, 0x7e, 0x14, 0xb0, 0x4f, 0x29, 0xe9, 0xc2, 0xfd,
0xc1, 0xb7, 0x53, 0x36, 0x5f, 0x6d, 0x71, 0x54, 0x76, 0xd3, 0x8c, 0x43, 0xd9, 0xfe, 0x6a, 0x8b,
0x6b, 0x57, 0x52, 0x23, 0xfa, 0xab, 0x2d, 0x01, 0xdc, 0xfa, 0x6a, 0x8b, 0xc7, 0xb3, 0xf7, 0xab,
0x2d, 0x5e, 0x6b, 0xc1, 0xaf, 0xb6, 0x84, 0x35, 0xa8, 0xc9, 0xa7, 0x29, 0x82, 0xdc, 0x55, 0xef,
0x65, 0xd1, 0xdd, 0x64, 0x7f, 0x76, 0x1d, 0x15, 0x62, 0xfa, 0x95, 0x9c, 0xb8, 0x11, 0xd7, 0xe3,
0x99, 0x3a, 0xb7, 0xe2, 0x36, 0x7a, 0xf3, 0xca, 0xf7, 0x4f, 0xd4, 0xda, 0x4b, 0x4f, 0x36, 0xac,
0x14, 0x5f, 0xec, 0x59, 0x0b, 0x4d, 0x1e, 0xb5, 0x05, 0xbb, 0xe5, 0x9f, 0xf6, 0x83, 0x89, 0xea,
0xd6, 0x84, 0x6a, 0xf4, 0x51, 0x97, 0x21, 0xd4, 0xe4, 0x1b, 0xbd, 0x79, 0x62, 0x92, 0x93, 0xbe,
0x65, 0x6b, 0xf7, 0x30, 0xe6, 0xb6, 0xf5, 0x66, 0x7f, 0x05, 0xe5, 0x7e, 0xa9, 0x92, 0x5a, 0xdb,
0xbd, 0x68, 0xe7, 0xf5, 0x2e, 0x53, 0x63, 0xa7, 0x99, 0x47, 0x7d, 0xf1, 0x50, 0x7a, 0x63, 0x4f,
0xf0, 0x5d, 0xe9, 0x8d, 0x77, 0x92, 0xff, 0xe4, 0x7a, 0x4a, 0xaa, 0x2c, 0xff, 0x3a, 0x88, 0x6e,
0x92, 0x65, 0x51, 0x71, 0xf0, 0x59, 0x5f, 0xcb, 0x28, 0x1e, 0x3e, 0xbf, 0xb6, 0x9e, 0x2a, 0xd4,
0x7f, 0x0c, 0xa2, 0x5b, 0x81, 0x42, 0xc9, 0x00, 0xb9, 0x86, 0x75, 0x37, 0x50, 0x7e, 0x78, 0x7d,
0x45, 0x6a, 0xba, 0xb7, 0xf1, 0x71, 0xfb, 0x73, 0x26, 0x01, 0xdb, 0x63, 0xfa, 0x73, 0x26, 0xdd,
0x5a, 0x78, 0x0b, 0xaa, 0x4e, 0x4a, 0xd4, 0xca, 0xc8, 0xb7, 0x05, 0x25, 0x72, 0x16, 0xb4, 0x22,
0x7a, 0xd8, 0xc9, 0xf9, 0x9c, 0xbc, 0x7c, 0x5b, 0xc4, 0xf9, 0x94, 0x76, 0x22, 0xe5, 0xdd, 0x4e,
0x34, 0x87, 0xb7, 0xee, 0x6a, 0xe9, 0x09, 0x6b, 0x96, 0x79, 0x8f, 0x29, 0x7d, 0x8d, 0x04, 0xb7,
0xee, 0x5a, 0x28, 0xe1, 0x4d, 0xe5, 0xb4, 0x21, 0x6f, 0x28, 0x95, 0x7d, 0xd2, 0x07, 0x45, 0x0b,
0x08, 0xed, 0x4d, 0x9f, 0x08, 0x3c, 0x0d, 0x59, 0x69, 0x9d, 0x0a, 0xac, 0xf7, 0xa4, 0x09, 0xb7,
0x63, 0xe0, 0x5f, 0x42, 0x3c, 0x85, 0x32, 0xe8, 0x56, 0x53, 0xbd, 0xdc, 0xda, 0xb4, 0xcf, 0xed,
0x36, 0xcb, 0x16, 0xf3, 0x5c, 0x35, 0x26, 0xe9, 0xd6, 0xa6, 0xba, 0xdd, 0x22, 0x1a, 0x6f, 0x5a,
0x1a, 0xb7, 0x22, 0xbd, 0x7c, 0x12, 0x36, 0xe3, 0x64, 0x95, 0x6b, 0xbd, 0x58, 0xba, 0x9e, 0x2a,
0x8c, 0x3a, 0xea, 0x89, 0x22, 0x69, 0xbd, 0x27, 0x8d, 0x77, 0x0f, 0x2d, 0xb7, 0x3a, 0x9e, 0x36,
0x3a, 0x6c, 0xb5, 0x42, 0x6a, 0xb3, 0xbf, 0x02, 0xde, 0xab, 0x55, 0x51, 0x55, 0xaf, 0x8b, 0x76,
0xd3, 0x2c, 0x1b, 0xae, 0x05, 0xc2, 0xa4, 0x81, 0x82, 0x7b, 0xb5, 0x1e, 0x98, 0x88, 0xe4, 0x66,
0x6f, 0x33, 0x1f, 0x76, 0xd9, 0x11, 0x54, 0xaf, 0x48, 0xb6, 0x69, 0xb4, 0xdf, 0x66, 0x3d, 0x6a,
0x5d, 0xdb, 0x51, 0xf8, 0xc1, 0xb5, 0x2a, 0xbc, 0xd1, 0x9b, 0x47, 0x97, 0x01, 0x04, 0x25, 0x66,
0x96, 0x7b, 0x94, 0x09, 0x67, 0x26, 0xb9, 0xdf, 0x41, 0xa1, 0x3d, 0x4b, 0xd9, 0x8d, 0xde, 0xa4,
0xd3, 0x19, 0x70, 0xef, 0x39, 0x96, 0x0d, 0x04, 0xcf, 0xb1, 0x10, 0x88, 0x9a, 0x4e, 0xfe, 0xae,
0x37, 0x6b, 0xf7, 0xa7, 0xbe, 0xa6, 0x53, 0xca, 0x16, 0x15, 0x6a, 0x3a, 0x2f, 0x8d, 0x46, 0x03,
0xed, 0x56, 0xbd, 0xba, 0xfe, 0x24, 0x64, 0x06, 0xbd, 0xbf, 0xbe, 0xd6, 0x8b, 0x45, 0x33, 0x8a,
0x71, 0x98, 0xce, 0x53, 0xee, 0x9b, 0x51, 0x2c, 0x1b, 0x35, 0x12, 0x9a, 0x51, 0xda, 0x28, 0x55,
0xbd, 0x3a, 0x47, 0xd8, 0x9f, 0x86, 0xab, 0x27, 0x99, 0x7e, 0xd5, 0xd3, 0x6c, 0xeb, 0xd8, 0x35,
0xd7, 0x21, 0xc3, 0x2f, 0xd4, 0x62, 0xd9, 0x13, 0xdb, 0xe2, 0xe5, 0x4a, 0x0c, 0x86, 0x46, 0x1d,
0x4a, 0x01, 0x1f, 0x27, 0xd4, 0x5c, 0x73, 0x32, 0x5c, 0x14, 0x10, 0x97, 0x71, 0x9e, 0x78, 0x17,
0xa7, 0xc2, 0x60, 0x8b, 0x0c, 0x2d, 0x4e, 0x49, 0x0d, 0x74, 0xa8, 0xef, 0xbe, 0x16, 0xe9, 0xe9,
0x0a, 0xfa, 0xfd, 0x43, 0xf7, 0xad, 0xc8, 0xc7, 0x3d, 0x48, 0x7c, 0xa8, 0xdf, 0x00, 0x7a, 0x5b,
0x5e, 0x3a, 0xfd, 0x38, 0x60, 0xca, 0x45, 0x43, 0x0b, 0x61, 0x5a, 0x05, 0x05, 0xb5, 0x4e, 0x70,
0x81, 0xff, 0x18, 0x56, 0xbe, 0xa0, 0x36, 0xf9, 0xa9, 0x40, 0x42, 0x41, 0xdd, 0x46, 0x51, 0x9e,
0x69, 0xaf, 0x83, 0x1e, 0x04, 0xf4, 0xed, 0xa5, 0xcf, 0xc3, 0x4e, 0x0e, 0xf5, 0x9c, 0x9d, 0x74,
0xe9, 0x9c, 0x62, 0x78, 0x0a, 0xba, 0x93, 0x2e, 0xfd, 0x87, 0x18, 0x6b, 0xbd, 0x58, 0x7c, 0x61,
0x20, 0xe6, 0xf0, 0xb6, 0x39, 0xc9, 0xf7, 0x14, 0x57, 0xc8, 0x5b, 0x47, 0xf9, 0x8f, 0xba, 0x41,
0x73, 0x3d, 0xf7, 0xb8, 0x64, 0x09, 0x54, 0x95, 0xfa, 0xc6, 0x9b, 0x7b, 0xff, 0x49, 0xc9, 0x46,
0xe8, 0x0b, 0x6f, 0xf7, 0xc2, 0x90, 0xb2, 0xfd, 0x65, 0xf4, 0xee, 0x01, 0x9b, 0x8d, 0x21, 0x9f,
0x0e, 0x7f, 0xe0, 0x5e, 0x88, 0x65, 0xb3, 0x51, 0xfd, 0xb3, 0xb6, 0x77, 0x83, 0x12, 0x9b, 0x2b,
0x7d, 0x3b, 0x70, 0xb6, 0x98, 0x8d, 0x79, 0xcc, 0xd1, 0x95, 0x3e, 0xf1, 0xfb, 0xa8, 0x16, 0x10,
0x57, 0xfa, 0x1c, 0x00, 0xd9, 0x9b, 0x94, 0x00, 0x5e, 0x7b, 0xb5, 0x20, 0x68, 0x4f, 0x01, 0x66,
0xd6, 0xd5, 0xf6, 0xea, 0xc4, 0x16, 0x5f, 0xc1, 0x33, 0x3a, 0x42, 0x4a, 0xcc, 0xba, 0x6d, 0xca,
0x04, 0x83, 0xac, 0xbe, 0xf8, 0xa2, 0xc5, 0x62, 0x3e, 0x8f, 0xcb, 0x15, 0x0a, 0x06, 0x55, 0x4b,
0x0b, 0x20, 0x82, 0xc1, 0x0b, 0x9a, 0x28, 0x6f, 0x1e, 0x73, 0x72, 0xb9, 0xc7, 0x4a, 0xb6, 0xe0,
0x69, 0x0e, 0xf8, 0xab, 0x06, 0xfa, 0x81, 0xda, 0x0c, 0x11, 0xe5, 0x14, 0x6b, 0xb2, 0x42, 0x41,
0xc8, 0xdb, 0x81, 0xe2, 0x4b, 0xa9, 0x15, 0x67, 0x25, 0x3e, 0x1d, 0x94, 0x56, 0x30, 0x44, 0x64,
0x85, 0x24, 0x8c, 0xda, 0xfe, 0x38, 0xcd, 0x67, 0xde, 0xb6, 0x3f, 0xb6, 0xbf, 0x33, 0x78, 0x8b,
0x06, 0xcc, 0xf8, 0x2e, 0x1f, 0x9a, 0xfc, 0x72, 0x90, 0x7a, 0x4b, 0xd2, 0xfb, 0xd0, 0x6d, 0x82,
0x18, 0xdf, 0xfd, 0x24, 0x72, 0xf5, 0xaa, 0x80, 0x1c, 0xa6, 0xcd, 0x1d, 0x38, 0x9f, 0x2b, 0x87,
0x08, 0xba, 0xc2, 0xa4, 0x19, 0x55, 0x85, 0xfc, 0x64, 0x91, 0x1f, 0x97, 0xec, 0x3c, 0xcd, 0xa0,
0x44, 0xa3, 0xaa, 0x54, 0xb7, 0xe4, 0xc4, 0xa8, 0xea, 0xe3, 0xcc, 0x65, 0x0a, 0x21, 0x75, 0x3e,
0xf7, 0x3b, 0x29, 0xe3, 0x04, 0x5f, 0xa6, 0x90, 0x36, 0xda, 0x18, 0xb1, 0x93, 0x16, 0xc0, 0x4d,
0xa4, 0x1f, 0x02, 0x2f, 0xd3, 0xa4, 0x1a, 0x03, 0x3f, 0x8e, 0xcb, 0x78, 0x0e, 0x1c, 0x4a, 0x1c,
0xe9, 0x0a, 0x19, 0x39, 0x0c, 0x11, 0xe9, 0x14, 0xab, 0x1c, 0xfe, 0x61, 0xf4, 0x7e, 0x3d, 0xd0,
0x43, 0xae, 0xbe, 0x4c, 0xff, 0x52, 0xfc, 0x49, 0x8b, 0xe1, 0x07, 0xda, 0xc6, 0x98, 0x97, 0x10,
0xcf, 0x1b, 0xdb, 0xef, 0xe9, 0xdf, 0x05, 0xb8, 0x39, 0xa8, 0x1b, 0xe4, 0x88, 0xf1, 0xf4, 0xbc,
0x5e, 0x57, 0xa9, 0x53, 0x2c, 0xd4, 0x20, 0xb6, 0x78, 0x14, 0xf8, 0x64, 0x80, 0x8f, 0x33, 0x03,
0x8d, 0x2d, 0x3d, 0x81, 0x22, 0xc3, 0x03, 0x8d, 0xa3, 0x2d, 0x00, 0x62, 0xa0, 0xf1, 0x82, 0x26,
0xba, 0x6c, 0xf1, 0x04, 0xc2, 0x95, 0x99, 0x40, 0xbf, 0xca, 0x4c, 0x9c, 0x77, 0x04, 0xb2, 0xe8,
0xfd, 0x43, 0x98, 0x9f, 0x41, 0x59, 0x5d, 0xa4, 0xc5, 0x5e, 0x3d, 0xc3, 0xc6, 0x7c, 0x81, 0xdf,
0xa2, 0x33, 0xc4, 0x48, 0x23, 0x44, 0x1a, 0x42, 0xa0, 0x66, 0x28, 0x33, 0xc0, 0x7e, 0x75, 0x14,
0xcf, 0x41, 0x7c, 0x00, 0x61, 0xb8, 0x46, 0x19, 0xb1, 0x20, 0x62, 0x28, 0x23, 0x61, 0xeb, 0x75,
0x23, 0xc3, 0x9c, 0xc0, 0xac, 0x8e, 0xb0, 0xf2, 0x38, 0x5e, 0xcd, 0x21, 0xe7, 0xca, 0x24, 0xda,
0x84, 0xb5, 0x4c, 0xfa, 0x79, 0x62, 0x13, 0xb6, 0x8f, 0x9e, 0x95, 0x74, 0x3b, 0x0f, 0xfe, 0x98,
0x95, 0x5c, 0xfe, 0xdd, 0x89, 0xd3, 0x32, 0x43, 0x49, 0xb7, 0xfb, 0x50, 0x1d, 0x92, 0x48, 0xba,
0xc3, 0x1a, 0xd6, 0x07, 0x9b, 0x9d, 0x32, 0xbc, 0x86, 0x52, 0xc7, 0xc9, 0xcb, 0x79, 0x9c, 0x66,
0x2a, 0x1a, 0xbe, 0x08, 0xd8, 0x26, 0x74, 0x88, 0x0f, 0x36, 0xf7, 0xd5, 0xb5, 0x3e, 0x71, 0x1d,
0x2e, 0x21, 0xda, 0x13, 0xee, 0xb0, 0x4f, 0xec, 0x09, 0x77, 0x6b, 0x99, 0xa5, 0x9a, 0x61, 0x05,
0xb7, 0x12, 0xc4, 0x36, 0x9b, 0xe2, 0x0d, 0x22, 0xcb, 0x26, 0x02, 0x89, 0xa5, 0x5a, 0x50, 0xc1,
0xcc, 0x6d, 0x06, 0xdb, 0x4d, 0xf3, 0x38, 0x4b, 0x7f, 0x8a, 0xef, 0x3e, 0x5b, 0x76, 0x1a, 0x82,
0x98, 0xdb, 0xfc, 0xa4, 0xcf, 0xd5, 0x1e, 0xf0, 0x49, 0x5a, 0x0f, 0xfd, 0x8f, 0x02, 0xcf, 0x4d,
0x10, 0xdd, 0xae, 0x2c, 0x52, 0xb9, 0xfa, 0xd9, 0x20, 0xba, 0x89, 0x1f, 0xeb, 0x56, 0x51, 0x8c,
0xeb, 0x94, 0xe4, 0x04, 0x12, 0x48, 0x0b, 0x3e, 0xfc, 0x34, 0xfc, 0xac, 0x10, 0x4e, 0x9c, 0xac,
0xf7, 0x50, 0xb3, 0xce, 0x6b, 0xeb, 0xb1, 0x64, 0x2c, 0xff, 0x20, 0xd3, 0x69, 0x05, 0xa5, 0x9a,
0x29, 0xf7, 0x80, 0xa3, 0xde, 0x69, 0x71, 0x23, 0x0b, 0xac, 0x2b, 0x4a, 0xf4, 0xce, 0xb0, 0x86,
0xd9, 0xdd, 0xb1, 0xb8, 0x13, 0xa8, 0x58, 0xb6, 0x04, 0x71, 0xfd, 0xed, 0x29, 0x69, 0xcc, 0xa2,
0x88, 0xdd, 0x1d, 0x9a, 0x36, 0xe9, 0x46, 0xdb, 0xed, 0x56, 0xbe, 0xda, 0xc7, 0x67, 0xe4, 0x1e,
0x4b, 0x02, 0x23, 0xd2, 0x8d, 0x00, 0x6e, 0xed, 0x7e, 0x96, 0x2c, 0x9e, 0x26, 0x71, 0xc5, 0x8f,
0xe3, 0x55, 0xc6, 0xe2, 0xa9, 0x98, 0xd7, 0xf1, 0xee, 0x67, 0xc3, 0x8c, 0x6c, 0x88, 0xda, 0xfd,
0xa4, 0x60, 0xb3, 0xb2, 0x53, 0x7f, 0x67, 0x4a, 0x5d, 0x2d, 0xbc, 0x8b, 0x72, 0x24, 0x51, 0x5e,
0x7c, 0xad, 0xf0, 0x5e, 0x18, 0x32, 0xaf, 0x44, 0x49, 0x91, 0x48, 0x43, 0x6e, 0xf9, 0x74, 0x9c,
0x04, 0xe4, 0x76, 0x80, 0x30, 0x9f, 0x49, 0x90, 0xbf, 0x37, 0x7f, 0x2a, 0x81, 0xab, 0x4f, 0xd7,
0x3e, 0xf5, 0xe9, 0xda, 0xd0, 0xc8, 0xfe, 0x0e, 0xd9, 0x7a, 0x4f, 0x5a, 0x7a, 0x7d, 0x71, 0xfb,
0x7f, 0xbf, 0xbe, 0x31, 0xf8, 0xf9, 0xd7, 0x37, 0x06, 0xff, 0xff, 0xf5, 0x8d, 0xc1, 0xcf, 0xbe,
0xb9, 0xf1, 0xce, 0xcf, 0xbf, 0xb9, 0xf1, 0xce, 0x2f, 0xbe, 0xb9, 0xf1, 0xce, 0x57, 0xef, 0xaa,
0x3f, 0x58, 0x76, 0xf6, 0x2b, 0xe2, 0xcf, 0x8e, 0x3d, 0xff, 0x65, 0x00, 0x00, 0x00, 0xff, 0xff,
0xf5, 0x3b, 0x43, 0x25, 0xd4, 0x6c, 0x00, 0x00,
}
// This is a compile-time assertion to ensure that this generated file
@ -646,6 +649,9 @@ type ClientCommandsHandler interface {
// 12D3KooWA8EXV3KjBxEU5EnsPfneLx84vMWAtTBQBeyooN82KSuS -> hello.any
NameServiceResolveAnyId(context.Context, *pb.RpcNameServiceResolveAnyIdRequest) *pb.RpcNameServiceResolveAnyIdResponse
BroadcastPayloadEvent(context.Context, *pb.RpcBroadcastPayloadEventRequest) *pb.RpcBroadcastPayloadEventResponse
DeviceSetName(context.Context, *pb.RpcDeviceSetNameRequest) *pb.RpcDeviceSetNameResponse
DeviceList(context.Context, *pb.RpcDeviceListRequest) *pb.RpcDeviceListResponse
DeviceNetworkStateSet(context.Context, *pb.RpcDeviceNetworkStateSetRequest) *pb.RpcDeviceNetworkStateSetResponse
}
func registerClientCommandsHandler(srv ClientCommandsHandler) {
@ -5572,6 +5578,66 @@ func BroadcastPayloadEvent(b []byte) (resp []byte) {
return resp
}
func DeviceSetName(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcDeviceSetNameResponse{Error: &pb.RpcDeviceSetNameResponseError{Code: pb.RpcDeviceSetNameResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcDeviceSetNameRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcDeviceSetNameResponse{Error: &pb.RpcDeviceSetNameResponseError{Code: pb.RpcDeviceSetNameResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.DeviceSetName(context.Background(), in).Marshal()
return resp
}
func DeviceList(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcDeviceListResponse{Error: &pb.RpcDeviceListResponseError{Code: pb.RpcDeviceListResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcDeviceListRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcDeviceListResponse{Error: &pb.RpcDeviceListResponseError{Code: pb.RpcDeviceListResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.DeviceList(context.Background(), in).Marshal()
return resp
}
func DeviceNetworkStateSet(b []byte) (resp []byte) {
defer func() {
if PanicHandler != nil {
if r := recover(); r != nil {
resp, _ = (&pb.RpcDeviceNetworkStateSetResponse{Error: &pb.RpcDeviceNetworkStateSetResponseError{Code: pb.RpcDeviceNetworkStateSetResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
PanicHandler(r)
}
}
}()
in := new(pb.RpcDeviceNetworkStateSetRequest)
if err := in.Unmarshal(b); err != nil {
resp, _ = (&pb.RpcDeviceNetworkStateSetResponse{Error: &pb.RpcDeviceNetworkStateSetResponseError{Code: pb.RpcDeviceNetworkStateSetResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
return resp
}
resp, _ = clientCommandsHandler.DeviceNetworkStateSet(context.Background(), in).Marshal()
return resp
}
var PanicHandler func(v interface{})
func CommandAsync(cmd string, data []byte, callback func(data []byte)) {
@ -6070,6 +6136,12 @@ func CommandAsync(cmd string, data []byte, callback func(data []byte)) {
cd = NameServiceResolveAnyId(data)
case "BroadcastPayloadEvent":
cd = BroadcastPayloadEvent(data)
case "DeviceSetName":
cd = DeviceSetName(data)
case "DeviceList":
cd = DeviceList(data)
case "DeviceNetworkStateSet":
cd = DeviceNetworkStateSet(data)
default:
log.Errorf("unknown command type: %s\n", cmd)
}
@ -9536,3 +9608,45 @@ func (h *ClientCommandsHandlerProxy) BroadcastPayloadEvent(ctx context.Context,
call, _ := actualCall(ctx, req)
return call.(*pb.RpcBroadcastPayloadEventResponse)
}
func (h *ClientCommandsHandlerProxy) DeviceSetName(ctx context.Context, req *pb.RpcDeviceSetNameRequest) *pb.RpcDeviceSetNameResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.DeviceSetName(ctx, req.(*pb.RpcDeviceSetNameRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "DeviceSetName", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcDeviceSetNameResponse)
}
func (h *ClientCommandsHandlerProxy) DeviceList(ctx context.Context, req *pb.RpcDeviceListRequest) *pb.RpcDeviceListResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.DeviceList(ctx, req.(*pb.RpcDeviceListRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "DeviceList", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcDeviceListResponse)
}
func (h *ClientCommandsHandlerProxy) DeviceNetworkStateSet(ctx context.Context, req *pb.RpcDeviceNetworkStateSetRequest) *pb.RpcDeviceNetworkStateSetResponse {
actualCall := func(ctx context.Context, req any) (any, error) {
return h.client.DeviceNetworkStateSet(ctx, req.(*pb.RpcDeviceNetworkStateSetRequest)), nil
}
for _, interceptor := range h.interceptors {
toCall := actualCall
currentInterceptor := interceptor
actualCall = func(ctx context.Context, req any) (any, error) {
return currentInterceptor(ctx, req, "DeviceNetworkStateSet", toCall)
}
}
call, _ := actualCall(ctx, req)
return call.(*pb.RpcDeviceNetworkStateSetResponse)
}

View file

@ -125,8 +125,8 @@ func (mw *Middleware) AccountMove(cctx context.Context, req *pb.RpcAccountMoveRe
}
}
func (mw *Middleware) AccountDelete(cctx context.Context, req *pb.RpcAccountDeleteRequest) *pb.RpcAccountDeleteResponse {
status, err := mw.applicationService.AccountDelete(cctx, req)
func (mw *Middleware) AccountDelete(cctx context.Context, _ *pb.RpcAccountDeleteRequest) *pb.RpcAccountDeleteResponse {
status, err := mw.applicationService.AccountDelete(cctx)
code := mapErrorCode(err,
errToCode(application.ErrAccountIsAlreadyDeleted, pb.RpcAccountDeleteResponseError_ACCOUNT_IS_ALREADY_DELETED),
errToCode(net.ErrUnableToConnect, pb.RpcAccountDeleteResponseError_UNABLE_TO_CONNECT),

View file

@ -53,6 +53,7 @@ import (
"github.com/anyproto/anytype-heart/core/configfetcher"
"github.com/anyproto/anytype-heart/core/debug"
"github.com/anyproto/anytype-heart/core/debug/profiler"
"github.com/anyproto/anytype-heart/core/device"
"github.com/anyproto/anytype-heart/core/files"
"github.com/anyproto/anytype-heart/core/files/fileacl"
"github.com/anyproto/anytype-heart/core/files/fileobject"
@ -72,12 +73,15 @@ import (
"github.com/anyproto/anytype-heart/core/notifications"
"github.com/anyproto/anytype-heart/core/payments"
paymentscache "github.com/anyproto/anytype-heart/core/payments/cache"
"github.com/anyproto/anytype-heart/core/peerstatus"
"github.com/anyproto/anytype-heart/core/recordsbatcher"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/core/subscription"
"github.com/anyproto/anytype-heart/core/syncstatus"
"github.com/anyproto/anytype-heart/core/syncstatus/detailsupdater"
"github.com/anyproto/anytype-heart/core/syncstatus/nodestatus"
"github.com/anyproto/anytype-heart/core/syncstatus/spacesyncstatus"
"github.com/anyproto/anytype-heart/core/syncstatus/syncsubscriptions"
"github.com/anyproto/anytype-heart/core/wallet"
"github.com/anyproto/anytype-heart/metrics"
"github.com/anyproto/anytype-heart/pkg/lib/core"
@ -86,6 +90,7 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/localstore/filestore"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/ftsearch"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore/oldstore"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/space"
"github.com/anyproto/anytype-heart/space/coordinatorclient"
@ -201,7 +206,9 @@ func Bootstrap(a *app.App, components ...app.Component) {
// Data storages
Register(clientds.New()).
Register(debugstat.New()).
Register(ftsearch.New()).
// Register(ftsearch.BleveNew()).
Register(ftsearch.TantivyNew()).
Register(oldstore.New()).
Register(objectstore.New()).
Register(backlinks.New()).
Register(filestore.New()).
@ -229,6 +236,7 @@ func Bootstrap(a *app.App, components ...app.Component) {
Register(virtualspaceservice.New()).
Register(spacecore.New()).
Register(idresolver.New()).
Register(device.New()).
Register(localdiscovery.New()).
Register(peermanager.New()).
Register(typeprovider.New()).
@ -259,7 +267,8 @@ func Bootstrap(a *app.App, components ...app.Component) {
Register(treemanager.New()).
Register(block.New()).
Register(indexer.New()).
Register(detailsupdater.NewUpdater()).
Register(detailsupdater.New()).
Register(session.NewHookRunner()).
Register(spacesyncstatus.NewSpaceSyncStatus()).
Register(nodestatus.NewNodeStatus()).
Register(syncstatus.New()).
@ -272,24 +281,27 @@ func Bootstrap(a *app.App, components ...app.Component) {
Register(debug.New()).
Register(collection.New()).
Register(subscription.New()).
Register(syncsubscriptions.New()).
Register(builtinobjects.New()).
Register(bookmark.New()).
Register(importer.New()).
Register(decorator.New()).
Register(objectcreator.NewCreator()).
Register(kanban.New()).
Register(device.NewDevices()).
Register(editor.NewObjectFactory()).
Register(objectgraph.NewBuilder()).
Register(account.New()).
Register(profiler.New()).
Register(identity.New(30*time.Second, 10*time.Second)).
Register(templateservice.New()).
Register(notifications.New()).
Register(notifications.New(time.Second * 10)).
Register(paymentserviceclient.New()).
Register(nameservice.New()).
Register(nameserviceclient.New()).
Register(payments.New()).
Register(paymentscache.New())
Register(paymentscache.New()).
Register(peerstatus.New())
}
func MiddlewareVersion() string {

View file

@ -104,8 +104,6 @@ func (s *Service) handleCustomStorageLocation(req *pb.RpcAccountCreateRequest, a
}
func (s *Service) setAccountAndProfileDetails(ctx context.Context, req *pb.RpcAccountCreateRequest, newAcc *model.Account) error {
newAcc.Name = req.Name
personalSpaceId := app.MustComponent[account.Service](s.app).PersonalSpaceID()
var err error
newAcc.Info, err = app.MustComponent[account.Service](s.app).GetInfo(ctx, personalSpaceId)
@ -138,7 +136,6 @@ func (s *Service) setAccountAndProfileDetails(ctx context.Context, req *pb.RpcAc
if err != nil {
log.Warnf("can't add avatar: %v", err)
} else {
newAcc.Avatar = &model.AccountAvatar{Avatar: &model.AccountAvatarAvatarOfImage{Image: &model.BlockContentFile{Hash: hash}}}
profileDetails = append(profileDetails, &model.Detail{
Key: bundle.RelationKeyIconImage.String(),
Value: pbtypes.String(hash),

View file

@ -6,7 +6,6 @@ import (
"github.com/anyproto/anytype-heart/core/anytype/account"
"github.com/anyproto/anytype-heart/core/configfetcher"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/space/spacecore"
)
@ -16,7 +15,7 @@ var (
ErrAccountIsActive = errors.New("account is active")
)
func (s *Service) AccountDelete(ctx context.Context, req *pb.RpcAccountDeleteRequest) (*model.AccountStatus, error) {
func (s *Service) AccountDelete(ctx context.Context) (*model.AccountStatus, error) {
s.lock.RLock()
defer s.lock.RUnlock()
var (

View file

@ -24,8 +24,7 @@ func (s *Service) AccountRecover() error {
Value: &pb.EventMessageValueOfAccountShow{
AccountShow: &pb.EventAccountShow{
Account: &model.Account{
Id: res.Identity.GetPublic().Account(),
Name: "",
Id: res.Identity.GetPublic().Account(),
},
},
},

View file

@ -71,6 +71,7 @@ func (s *Service) AccountSelect(ctx context.Context, req *pb.RpcAccountSelectReq
if err := s.stop(); err != nil {
return nil, errors.Join(ErrFailedToStopApplication, err)
}
metrics.Service.SetWorkingDir(req.RootPath, req.Id)
return s.start(ctx, req.Id, req.RootPath, req.DisableLocalNetworkSync, req.PreferYamuxTransport, req.NetworkMode, req.NetworkCustomConfigFilePath)
}

View file

@ -137,8 +137,8 @@ func (uw *UpdateWatcher) backlinksUpdateHandler() {
select {
case <-closedCh:
l.Lock()
defer l.Unlock()
process()
l.Unlock()
return
case <-time.After(aggregationInterval):
l.Lock()
@ -185,9 +185,9 @@ func (uw *UpdateWatcher) updateBackLinksInObject(id string, backlinksUpdate *bac
return
}
updateBacklinks := func(current *types.Struct, backlinksChange *backLinksUpdate) (*types.Struct, error) {
updateBacklinks := func(current *types.Struct, backlinksChange *backLinksUpdate) (*types.Struct, bool, error) {
if current == nil || current.Fields == nil {
return nil, objectstore.ErrDetailsNotChanged
return nil, false, nil
}
backlinks := pbtypes.GetStringList(current, bundle.RelationKeyBacklinks.String())
@ -202,11 +202,11 @@ func (uw *UpdateWatcher) updateBackLinksInObject(id string, backlinksUpdate *bac
}
current.Fields[bundle.RelationKeyBacklinks.String()] = pbtypes.StringList(backlinks)
return current, nil
return current, true, nil
}
err = spc.DoLockedIfNotExists(id, func() error {
return uw.store.ModifyObjectDetails(id, func(details *types.Struct) (*types.Struct, error) {
return uw.store.ModifyObjectDetails(id, func(details *types.Struct) (*types.Struct, bool, error) {
return updateBacklinks(details, backlinksUpdate)
})
})

View file

@ -190,9 +190,7 @@ func (s *Service) CreateCollection(details *types.Struct, flags []*model.Interna
newState := state.NewDoc("", nil).NewState().SetDetails(details)
tmpls := []template.StateTransformer{
template.WithRequiredRelations(),
}
tmpls := []template.StateTransformer{}
blockContent := template.MakeCollectionDataviewContent()
tmpls = append(tmpls,

View file

@ -127,18 +127,6 @@ func (s *Service) TurnInto(
})
}
func (s *Service) SimplePaste(contextId string, anySlot []*model.Block) (err error) {
var blocks []simple.Block
for _, b := range anySlot {
blocks = append(blocks, simple.New(b))
}
return cache.DoState(s, contextId, func(s *state.State, b basic.CommonOperations) error {
return b.PasteBlocks(s, "", model.Block_Inner, blocks)
})
}
func (s *Service) ReplaceBlock(ctx session.Context, req pb.RpcBlockReplaceRequest) (newId string, err error) {
err = cache.Do(s, req.ContextId, func(b basic.Replaceable) error {
newId, err = b.Replace(ctx, req.BlockId, req.Block)

View file

@ -8,6 +8,7 @@ import (
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/editor/template"
"github.com/anyproto/anytype-heart/core/block/migration"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/relationutils"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/database"
@ -17,6 +18,9 @@ import (
"github.com/anyproto/anytype-heart/util/slice"
)
// required relations for archive beside the bundle.RequiredInternalRelations
var archiveRequiredRelations = []domain.RelationKey{}
type Archive struct {
smartblock.SmartBlock
collection.Collection
@ -35,6 +39,7 @@ func NewArchive(
}
func (p *Archive) Init(ctx *smartblock.InitContext) (err error) {
ctx.RequiredInternalRelationKeys = append(ctx.RequiredInternalRelationKeys, archiveRequiredRelations...)
if err = p.SmartBlock.Init(ctx); err != nil {
return
}
@ -67,29 +72,28 @@ func (p *Archive) Relations(_ *state.State) relationutils.Relations {
return nil
}
func (p *Archive) updateObjects(info smartblock.ApplyInfo) (err error) {
func (p *Archive) updateObjects(_ smartblock.ApplyInfo) (err error) {
archivedIds, err := p.GetIds()
if err != nil {
return
}
records, err := p.objectStore.Query(database.Query{
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeyIsArchived.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Bool(true),
},
{
RelationKey: bundle.RelationKeySpaceId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(p.SpaceID()),
},
records, err := p.objectStore.QueryRaw(&database.Filters{FilterObj: database.FiltersAnd{
database.FilterEq{
Key: bundle.RelationKeyIsArchived.String(),
Cond: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Bool(true),
},
})
database.FilterEq{
Key: bundle.RelationKeySpaceId.String(),
Cond: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(p.SpaceID()),
},
}}, 0, 0)
if err != nil {
return
}
var storeArchivedIds = make([]string, 0, len(records))
for _, rec := range records {
storeArchivedIds = append(storeArchivedIds, pbtypes.GetString(rec.Details, bundle.RelationKeyId.String()))

View file

@ -17,7 +17,7 @@ func NewArchiveTest(ctrl *gomock.Controller) (*Archive, error) {
sb := smarttest.New("root")
objectStore := testMock.NewMockObjectStore(ctrl)
objectStore.EXPECT().GetDetails(gomock.Any()).AnyTimes()
objectStore.EXPECT().Query(gomock.Any()).AnyTimes()
objectStore.EXPECT().QueryRaw(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
a := &Archive{
SmartBlock: sb,
Collection: collection.NewCollection(sb, objectStore),

View file

@ -3,13 +3,13 @@ package basic
import (
"fmt"
"github.com/globalsign/mgo/bson"
"github.com/gogo/protobuf/types"
"github.com/samber/lo"
"github.com/anyproto/anytype-heart/core/block/editor/converter"
"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"
"github.com/anyproto/anytype-heart/core/block/editor/template"
"github.com/anyproto/anytype-heart/core/block/restriction"
"github.com/anyproto/anytype-heart/core/block/simple"
@ -19,6 +19,8 @@ import (
relationblock "github.com/anyproto/anytype-heart/core/block/simple/relation"
"github.com/anyproto/anytype-heart/core/block/simple/text"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/domain/objectorigin"
"github.com/anyproto/anytype-heart/core/files/fileobject"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
@ -52,7 +54,6 @@ type CommonOperations interface {
FeaturedRelationAdd(ctx session.Context, relations ...string) error
FeaturedRelationRemove(ctx session.Context, relations ...string) error
PasteBlocks(s *state.State, targetBlockId string, position model.BlockPosition, blocks []simple.Block) (err error)
ReplaceLink(oldId, newId string) error
ExtractBlocksToObjects(ctx session.Context, oc ObjectCreator, tsc TemplateStateCreator, req pb.RpcBlockListConvertToObjectsRequest) (linkIds []string, err error)
@ -102,19 +103,22 @@ func NewBasic(
sb smartblock.SmartBlock,
objectStore objectstore.ObjectStore,
layoutConverter converter.LayoutConverter,
fileObjectService fileobject.Service,
) AllOperations {
return &basic{
SmartBlock: sb,
objectStore: objectStore,
layoutConverter: layoutConverter,
SmartBlock: sb,
objectStore: objectStore,
layoutConverter: layoutConverter,
fileObjectService: fileObjectService,
}
}
type basic struct {
smartblock.SmartBlock
objectStore objectstore.ObjectStore
layoutConverter converter.LayoutConverter
objectStore objectstore.ObjectStore
layoutConverter converter.LayoutConverter
fileObjectService fileobject.Service
}
func (bs *basic) CreateBlock(s *state.State, req pb.RpcBlockCreateRequest) (id string, err error) {
@ -148,7 +152,7 @@ func (bs *basic) CreateBlock(s *state.State, req pb.RpcBlockCreateRequest) (id s
func (bs *basic) Duplicate(srcState, destState *state.State, targetBlockId string, position model.BlockPosition, blockIds []string) (newIds []string, err error) {
blockIds = srcState.SelectRoots(blockIds)
for _, id := range blockIds {
copyId, e := copyBlocks(srcState, destState, id)
copyId, e := bs.copyBlocks(srcState, destState, id)
if e != nil {
return nil, e
}
@ -168,7 +172,7 @@ type duplicatable interface {
Duplicate(s *state.State) (newId string, visitedIds []string, blocks []simple.Block, err error)
}
func copyBlocks(srcState, destState *state.State, sourceId string) (id string, err error) {
func (bs *basic) copyBlocks(srcState, destState *state.State, sourceId string) (id string, err error) {
b := srcState.Pick(sourceId)
if b == nil {
return "", smartblock.ErrSimpleBlockNotFound
@ -189,13 +193,37 @@ func copyBlocks(srcState, destState *state.State, sourceId string) (id string, e
result := simple.New(m)
destState.Add(result)
for i, childrenId := range result.Model().ChildrenIds {
if result.Model().ChildrenIds[i], err = copyBlocks(srcState, destState, childrenId); err != nil {
if result.Model().ChildrenIds[i], err = bs.copyBlocks(srcState, destState, childrenId); err != nil {
return
}
}
if f, ok := result.Model().Content.(*model.BlockContentOfFile); ok && srcState.SpaceID() != destState.SpaceID() {
bs.processFileBlock(f, destState.SpaceID())
}
return result.Model().Id, nil
}
func (bs *basic) processFileBlock(f *model.BlockContentOfFile, spaceId string) {
fileId, err := bs.fileObjectService.GetFileIdFromObject(f.File.TargetObjectId)
if err != nil {
log.Errorf("failed to get fileId: %v", err)
return
}
objectId, err := bs.fileObjectService.CreateFromImport(
domain.FullFileId{SpaceId: spaceId, FileId: fileId.FileId},
objectorigin.ObjectOrigin{Origin: model.ObjectOrigin_clipboard},
)
if err != nil {
log.Errorf("failed to create file object: %v", err)
return
}
f.File.TargetObjectId = objectId
}
func (bs *basic) Unlink(ctx session.Context, ids ...string) (err error) {
s := bs.NewStateCtx(ctx)
@ -236,6 +264,11 @@ func (bs *basic) Move(srcState, destState *state.State, targetBlockId string, po
}
}
targetBlockId, position, err = table.CheckTableBlocksMove(srcState, targetBlockId, position, blockIds)
if err != nil {
return err
}
var replacementCandidate simple.Block
for _, id := range blockIds {
if b := srcState.Pick(id); b != nil {
@ -472,31 +505,3 @@ func (bs *basic) ReplaceLink(oldId, newId string) error {
}
return bs.Apply(s)
}
func (bs *basic) PasteBlocks(s *state.State, targetBlockID string, position model.BlockPosition, blocks []simple.Block) (err error) {
childIdsRewrite := make(map[string]string)
for _, b := range blocks {
for i, cID := range b.Model().ChildrenIds {
newID := bson.NewObjectId().Hex()
childIdsRewrite[cID] = newID
b.Model().ChildrenIds[i] = newID
}
}
for _, b := range blocks {
var child bool
if newID, ok := childIdsRewrite[b.Model().Id]; ok {
b.Model().Id = newID
child = true
} else {
b.Model().Id = bson.NewObjectId().Hex()
}
s.Add(b)
if !child {
err := s.InsertTo(targetBlockID, position, b.Model().Id)
if err != nil {
return err
}
}
}
return nil
}

View file

@ -1,19 +1,26 @@
package basic
import (
"errors"
"math/rand"
"testing"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/editor/converter"
"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/block/editor/table"
"github.com/anyproto/anytype-heart/core/block/editor/template"
"github.com/anyproto/anytype-heart/core/block/restriction"
"github.com/anyproto/anytype-heart/core/block/simple"
"github.com/anyproto/anytype-heart/core/block/simple/text"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/files/fileobject"
"github.com/anyproto/anytype-heart/core/files/fileobject/mock_fileobject"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
@ -38,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())
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"}}},
@ -52,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())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
s := sb.NewState()
id, err := b.CreateBlock(s, pb.RpcBlockCreateRequest{
TargetId: template.TitleBlockId,
@ -73,29 +80,123 @@ 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())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
_, err := b.CreateBlock(sb.NewState(), pb.RpcBlockCreateRequest{})
assert.ErrorIs(t, err, restriction.ErrRestricted)
})
}
func TestBasic_Duplicate(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", ChildrenIds: []string{"3"}})).
AddBlock(simple.New(&model.Block{Id: "3"}))
t.Run("dup blocks to same state", func(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", ChildrenIds: []string{"3"}})).
AddBlock(simple.New(&model.Block{Id: "3"}))
st := sb.NewState()
newIds, err := NewBasic(sb, nil, converter.NewLayoutConverter()).Duplicate(st, st, "", 0, []string{"2"})
require.NoError(t, err)
st := sb.NewState()
newIds, err := NewBasic(sb, nil, converter.NewLayoutConverter(), nil).Duplicate(st, st, "", 0, []string{"2"})
require.NoError(t, err)
err = sb.Apply(st)
require.NoError(t, err)
err = sb.Apply(st)
require.NoError(t, err)
require.Len(t, newIds, 1)
s := sb.NewState()
assert.Len(t, s.Pick(newIds[0]).Model().ChildrenIds, 1)
assert.Len(t, sb.Blocks(), 5)
})
for _, tc := range []struct {
name string
fos func() fileobject.Service
spaceIds []string
targets []string
}{
{
name: "dup file block - same space",
fos: func() fileobject.Service {
return nil
},
spaceIds: []string{"space1", "space1"},
targets: []string{"file1_space1", "file2_space1"},
},
{
name: "dup file block - other space",
fos: func() fileobject.Service {
fos := mock_fileobject.NewMockService(t)
fos.EXPECT().GetFileIdFromObject("file1_space1").Return(domain.FullFileId{SpaceId: "space1", FileId: "file1"}, nil)
fos.EXPECT().CreateFromImport(domain.FullFileId{SpaceId: "space2", FileId: "file1"}, mock.Anything).Return("file1_space2", nil)
fos.EXPECT().GetFileIdFromObject("file2_space1").Return(domain.FullFileId{SpaceId: "space1", FileId: "file2"}, nil)
fos.EXPECT().CreateFromImport(domain.FullFileId{SpaceId: "space2", FileId: "file2"}, mock.Anything).Return("file2_space2", nil)
return fos
},
spaceIds: []string{"space1", "space2"},
targets: []string{"file1_space2", "file2_space2"},
},
{
name: "dup file block - no target change if failed to retrieve file id",
fos: func() fileobject.Service {
fos := mock_fileobject.NewMockService(t)
fos.EXPECT().GetFileIdFromObject(mock.Anything).Return(domain.FullFileId{}, errors.New("no such file")).Times(2)
return fos
},
spaceIds: []string{"space1", "space2"},
targets: []string{"file1_space1", "file2_space1"},
},
{
name: "dup file block - no target change if failed to create file object",
fos: func() fileobject.Service {
fos := mock_fileobject.NewMockService(t)
fos.EXPECT().GetFileIdFromObject("file1_space1").Return(domain.FullFileId{SpaceId: "space1", FileId: "file1"}, nil)
fos.EXPECT().GetFileIdFromObject("file2_space1").Return(domain.FullFileId{SpaceId: "space1", FileId: "file2"}, nil)
fos.EXPECT().CreateFromImport(mock.Anything, mock.Anything).Return("", errors.New("creation failure"))
return fos
},
spaceIds: []string{"space1", "space2"},
targets: []string{"file1_space1", "file2_space1"},
},
} {
t.Run(tc.name, func(t *testing.T) {
// given
source := smarttest.New("source").
AddBlock(simple.New(&model.Block{Id: "source", ChildrenIds: []string{"1", "f1"}})).
AddBlock(simple.New(&model.Block{Id: "1", ChildrenIds: []string{"f2"}})).
AddBlock(simple.New(&model.Block{Id: "f1", Content: &model.BlockContentOfFile{File: &model.BlockContentFile{TargetObjectId: "file1_space1"}}})).
AddBlock(simple.New(&model.Block{Id: "f2", Content: &model.BlockContentOfFile{File: &model.BlockContentFile{TargetObjectId: "file2_space1"}}}))
ss := source.NewState()
ss.SetDetail(bundle.RelationKeySpaceId.String(), pbtypes.String(tc.spaceIds[0]))
target := smarttest.New("target").
AddBlock(simple.New(&model.Block{Id: "target"}))
ts := target.NewState()
ts.SetDetail(bundle.RelationKeySpaceId.String(), pbtypes.String(tc.spaceIds[1]))
// when
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))
// then
assert.Len(t, newIds, 2)
ts = target.NewState()
root := ts.Pick("target")
assert.Equal(t, newIds, root.Model().ChildrenIds)
block1 := ts.Pick(newIds[0])
require.NotNil(t, block1)
blockChildren := block1.Model().ChildrenIds
assert.Len(t, blockChildren, 1)
for fbID, targetID := range map[string]string{newIds[1]: tc.targets[0], blockChildren[0]: tc.targets[1]} {
fb := ts.Pick(fbID)
assert.NotNil(t, fb)
f, ok := fb.Model().Content.(*model.BlockContentOfFile)
assert.True(t, ok)
assert.Equal(t, targetID, f.File.TargetObjectId)
}
})
}
require.Len(t, newIds, 1)
s := sb.NewState()
assert.Len(t, s.Pick(newIds[0]).Model().ChildrenIds, 1)
assert.Len(t, sb.Blocks(), 5)
}
func TestBasic_Unlink(t *testing.T) {
@ -105,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())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
err := b.Unlink(nil, "2")
require.NoError(t, err)
@ -119,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())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
err := b.Unlink(nil, "2", "3")
require.NoError(t, err)
@ -136,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())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
err := b.Move(st, st, "4", model.Block_Inner, []string{"3"})
@ -150,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())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
s := sb.NewState()
id1, err := b.CreateBlock(s, pb.RpcBlockCreateRequest{
TargetId: template.HeaderLayoutId,
@ -199,7 +300,7 @@ func TestBasic_Move(t *testing.T) {
},
),
)
basic := NewBasic(testDoc, nil, converter.NewLayoutConverter())
basic := NewBasic(testDoc, nil, converter.NewLayoutConverter(), nil)
state := testDoc.NewState()
// when
@ -215,7 +316,7 @@ func TestBasic_Move(t *testing.T) {
AddBlock(newTextBlock("1", "", nil)).
AddBlock(newTextBlock("2", "one", nil))
b := NewBasic(sb, nil, converter.NewLayoutConverter())
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)
@ -235,7 +336,7 @@ func TestBasic_Move(t *testing.T) {
AddBlock(firstBlock).
AddBlock(secondBlock)
b := NewBasic(sb, nil, converter.NewLayoutConverter())
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)
@ -249,7 +350,7 @@ func TestBasic_Move(t *testing.T) {
AddBlock(newTextBlock("1", "", nil)).
AddBlock(newTextBlock("2", "one", nil))
b := NewBasic(sb, nil, converter.NewLayoutConverter())
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)
@ -258,6 +359,152 @@ func TestBasic_Move(t *testing.T) {
})
}
func TestBasic_MoveTableBlocks(t *testing.T) {
getSB := func() *smarttest.SmartTest {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"upper", "table", "block"}})).
AddBlock(simple.New(&model.Block{Id: "table", ChildrenIds: []string{"columns", "rows"}, Content: &model.BlockContentOfTable{Table: &model.BlockContentTable{}}})).
AddBlock(simple.New(&model.Block{Id: "columns", ChildrenIds: []string{"column"}, Content: &model.BlockContentOfLayout{Layout: &model.BlockContentLayout{Style: model.BlockContentLayout_TableColumns}}})).
AddBlock(simple.New(&model.Block{Id: "column", ChildrenIds: []string{}, Content: &model.BlockContentOfTableColumn{TableColumn: &model.BlockContentTableColumn{}}})).
AddBlock(simple.New(&model.Block{Id: "rows", ChildrenIds: []string{"row", "row2"}, Content: &model.BlockContentOfLayout{Layout: &model.BlockContentLayout{Style: model.BlockContentLayout_TableRows}}})).
AddBlock(simple.New(&model.Block{Id: "row", ChildrenIds: []string{"column-row"}, Content: &model.BlockContentOfTableRow{TableRow: &model.BlockContentTableRow{IsHeader: false}}})).
AddBlock(simple.New(&model.Block{Id: "row2", ChildrenIds: []string{}, Content: &model.BlockContentOfTableRow{TableRow: &model.BlockContentTableRow{IsHeader: false}}})).
AddBlock(simple.New(&model.Block{Id: "column-row", ChildrenIds: []string{}})).
AddBlock(simple.New(&model.Block{Id: "block", ChildrenIds: []string{}})).
AddBlock(simple.New(&model.Block{Id: "upper", ChildrenIds: []string{}}))
return sb
}
for _, block := range []string{"columns", "rows", "column", "row", "column-row"} {
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)
st := sb.NewState()
// when
err := b.Move(st, st, "block", model.Block_Bottom, []string{block})
// then
assert.Error(t, err)
assert.True(t, errors.Is(err, table.ErrCannotMoveTableBlocks))
})
}
t.Run("no error on moving root table block", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
// when
err := b.Move(st, st, "block", model.Block_Bottom, []string{"table"})
// then
assert.NoError(t, err)
assert.Equal(t, []string{"upper", "block", "table"}, st.Pick("test").Model().ChildrenIds)
})
t.Run("no error on moving one row between another", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
// when
err := b.Move(st, st, "row2", model.Block_Bottom, []string{"row"})
// then
assert.NoError(t, err)
assert.Equal(t, []string{"row2", "row"}, st.Pick("rows").Model().ChildrenIds)
})
t.Run("moving rows with incorrect position leads to error", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
// when
err := b.Move(st, st, "row2", model.Block_Left, []string{"row"})
// then
assert.Error(t, err)
})
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)
st := sb.NewState()
// when
err := b.Move(st, st, "row2", model.Block_Top, []string{"row", "rows"})
// then
assert.Error(t, err)
})
t.Run("moving the row between itself leads to error", func(t *testing.T) {
// given
sb := getSB()
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
st := sb.NewState()
// when
err := b.Move(st, st, "row2", model.Block_Bottom, []string{"row2"})
// then
assert.Error(t, err)
})
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)
st := sb.NewState()
st.Unlink("columns")
// when
err := b.Move(st, st, "block", model.Block_Bottom, []string{"column-row"})
// then
assert.Error(t, err)
assert.True(t, errors.Is(err, table.ErrCannotMoveTableBlocks))
})
for _, block := range []string{"columns", "rows", "column", "row", "column-row"} {
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)
st := sb.NewState()
// when
err := b.Move(st, st, block, model.BlockPosition(rand.Intn(len(model.BlockPosition_name))), []string{"upper"})
// then
assert.NoError(t, err)
assert.Equal(t, []string{"table", "upper", "block"}, st.Pick("test").Model().ChildrenIds)
})
}
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)
st := sb.NewState()
st.Unlink("columns")
// when
err := b.Move(st, st, "rows", model.BlockPosition(rand.Intn(6)), []string{"upper"})
// then
assert.NoError(t, err)
assert.Equal(t, []string{"table", "upper", "block"}, st.Pick("test").Model().ChildrenIds)
})
}
func TestBasic_MoveToAnotherObject(t *testing.T) {
t.Run("basic", func(t *testing.T) {
sb1 := smarttest.New("test1")
@ -269,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())
b := NewBasic(sb1, nil, converter.NewLayoutConverter(), nil)
srcState := sb1.NewState()
destState := sb2.NewState()
@ -304,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())
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)
@ -314,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())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
fields := &types.Struct{
Fields: map[string]*types.Value{
@ -333,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())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
err := b.Update(nil, func(b simple.Block) error {
b.Model().BackgroundColor = "test"
@ -347,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())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
err := b.SetDivStyle(nil, model.BlockContentDiv_Dots, "2")
require.NoError(t, err)
@ -355,24 +602,6 @@ func TestBasic_SetDivStyle(t *testing.T) {
assert.Equal(t, model.BlockContentDiv_Dots, r.Pick("2").Model().GetDiv().Style)
}
func TestBasic_PasteBlocks(t *testing.T) {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
b := NewBasic(sb, nil, converter.NewLayoutConverter())
s := sb.NewState()
err := b.PasteBlocks(s, "", model.Block_Inner, []simple.Block{
simple.New(&model.Block{Id: "1", ChildrenIds: []string{"1.1"}}),
simple.New(&model.Block{Id: "1.1", ChildrenIds: []string{"1.1.1"}}),
simple.New(&model.Block{Id: "1.1.1"}),
simple.New(&model.Block{Id: "2", ChildrenIds: []string{"2.1"}}),
simple.New(&model.Block{Id: "2.1"}),
})
require.NoError(t, err)
require.Len(t, s.Blocks(), 6)
assert.Len(t, s.Pick(s.RootId()).Model().ChildrenIds, 2)
}
func TestBasic_SetRelationKey(t *testing.T) {
fillSb := func(sb *smarttest.SmartTest) {
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"1", "2"}})).
@ -385,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())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
err := b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
BlockId: "2",
Key: "testRelKey",
@ -407,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())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
require.Error(t, b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
BlockId: "1",
Key: "key",
@ -416,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())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
require.Error(t, b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
BlockId: "2",
Key: "not exists",
@ -428,11 +657,11 @@ func TestBasic_FeaturedRelationAdd(t *testing.T) {
sb := smarttest.New("test")
s := sb.NewState()
template.WithTitle(s)
s.AddBundledRelations(bundle.RelationKeyName)
s.AddBundledRelations(bundle.RelationKeyDescription)
s.AddBundledRelationLinks(bundle.RelationKeyName)
s.AddBundledRelationLinks(bundle.RelationKeyDescription)
require.NoError(t, sb.Apply(s))
b := NewBasic(sb, nil, converter.NewLayoutConverter())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
newRel := []string{bundle.RelationKeyDescription.String(), bundle.RelationKeyName.String()}
require.NoError(t, b.FeaturedRelationAdd(nil, newRel...))
@ -448,7 +677,7 @@ func TestBasic_FeaturedRelationRemove(t *testing.T) {
template.WithDescription(s)
require.NoError(t, sb.Apply(s))
b := NewBasic(sb, nil, converter.NewLayoutConverter())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
require.NoError(t, b.FeaturedRelationRemove(nil, bundle.RelationKeyDescription.String()))
res := sb.NewState()
@ -485,7 +714,7 @@ func TestBasic_ReplaceLink(t *testing.T) {
}
require.NoError(t, sb.Apply(s))
b := NewBasic(sb, nil, converter.NewLayoutConverter())
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
require.NoError(t, b.ReplaceLink(oldId, newId))
res := sb.NewState()

View file

@ -32,7 +32,7 @@ func newDUFixture(t *testing.T) *duFixture {
store := objectstore.NewStoreFixture(t)
b := NewBasic(sb, store, converter.NewLayoutConverter())
b := NewBasic(sb, store, converter.NewLayoutConverter(), nil)
return &duFixture{
sb: sb,

View file

@ -7,6 +7,7 @@ import (
"github.com/globalsign/mgo/bson"
"github.com/gogo/protobuf/types"
"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/simple"
"github.com/anyproto/anytype-heart/core/domain"
@ -23,6 +24,7 @@ type ObjectCreator interface {
type TemplateStateCreator interface {
CreateTemplateStateWithDetails(templateId string, details *types.Struct) (*state.State, error)
CreateTemplateStateFromSmartBlock(sb smartblock.SmartBlock, details *types.Struct) *state.State
}
// ExtractBlocksToObjects extracts child blocks from the object to separate objects and
@ -62,7 +64,7 @@ func (bs *basic) ExtractBlocksToObjects(
return nil, fmt.Errorf("create child object: %w", err)
}
linkID, err := bs.changeToBlockWithLink(newState, rootBlock, objectID)
linkID, err := bs.changeToBlockWithLink(newState, rootBlock, objectID, req.Block)
if err != nil {
return nil, fmt.Errorf("create link to object %s: %w", objectID, err)
}
@ -80,6 +82,11 @@ func (bs *basic) prepareObjectState(
if err != nil {
return nil, fmt.Errorf("prepare target details: %w", err)
}
if req.ContextId == req.TemplateId {
return creator.CreateTemplateStateFromSmartBlock(bs, details), nil
}
return creator.CreateTemplateStateWithDetails(req.TemplateId, details)
}
@ -98,40 +105,72 @@ func (bs *basic) prepareTargetObjectDetails(
}
func insertBlocksToState(
newState *state.State,
rootBlock simple.Block,
objState *state.State,
srcState *state.State,
srcSubtreeRoot simple.Block,
targetState *state.State,
) {
rootID := rootBlock.Model().Id
descendants := newState.Descendants(rootID)
newRoot, newBlocks := reassignSubtreeIds(rootID, append(descendants, rootBlock))
srcRootId := srcSubtreeRoot.Model().Id
descendants := srcState.Descendants(srcRootId)
newSubtreeRootId, newBlocks := copySubtreeOfBlocks(srcState, srcRootId, append(descendants, srcSubtreeRoot))
// remove descendant blocks from source object
removeBlocks(newState, descendants)
removeBlocks(srcState, descendants)
for _, b := range newBlocks {
objState.Add(b)
for _, newBlock := range newBlocks {
targetState.Add(newBlock)
}
rootB := objState.Pick(objState.RootId()).Model()
rootB.ChildrenIds = append(rootB.ChildrenIds, newRoot)
objState.Set(simple.New(rootB))
targetRootBlock := targetState.Pick(targetState.RootId()).Model()
if hasNoteLayout(targetState) {
targetRootBlock.ChildrenIds = append(targetRootBlock.ChildrenIds, newSubtreeRootId)
} else {
// text in newSubtree root has already been added to the title
children := targetState.Pick(newSubtreeRootId).Model().ChildrenIds
targetRootBlock.ChildrenIds = append(targetRootBlock.ChildrenIds, children...)
}
targetState.Set(simple.New(targetRootBlock))
}
func (bs *basic) changeToBlockWithLink(newState *state.State, blockToChange simple.Block, objectID string) (string, error) {
func (bs *basic) changeToBlockWithLink(newState *state.State, blockToReplace simple.Block, objectID string, linkBlock *model.Block) (string, error) {
return bs.CreateBlock(newState, pb.RpcBlockCreateRequest{
TargetId: blockToChange.Model().Id,
Block: &model.Block{
Content: &model.BlockContentOfLink{
Link: &model.BlockContentLink{
TargetBlockId: objectID,
Style: model.BlockContentLink_Page,
},
},
},
TargetId: blockToReplace.Model().Id,
Block: buildBlock(linkBlock, objectID),
Position: model.Block_Replace,
})
}
func buildBlock(b *model.Block, targetID string) (result *model.Block) {
fallback := &model.Block{
Content: &model.BlockContentOfLink{
Link: &model.BlockContentLink{
TargetBlockId: targetID,
Style: model.BlockContentLink_Page,
},
},
}
if b == nil {
return fallback
}
result = pbtypes.CopyBlock(b)
switch v := result.Content.(type) {
case *model.BlockContentOfLink:
v.Link.TargetBlockId = targetID
case *model.BlockContentOfBookmark:
v.Bookmark.TargetObjectId = targetID
case *model.BlockContentOfFile:
v.File.TargetObjectId = targetID
case *model.BlockContentOfDataview:
v.Dataview.TargetObjectId = targetID
default:
result = fallback
}
return
}
func removeBlocks(state *state.State, descendants []simple.Block) {
for _, b := range descendants {
state.Unlink(b.Model().Id)
@ -139,7 +178,9 @@ func removeBlocks(state *state.State, descendants []simple.Block) {
}
func createTargetObjectDetails(nameText string, layout model.ObjectTypeLayout) *types.Struct {
fields := map[string]*types.Value{}
fields := map[string]*types.Value{
bundle.RelationKeyLayout.String(): pbtypes.Int64(int64(layout)),
}
// Without this check title will be duplicated in template.WithNameToFirstBlock
if layout != model.ObjectType_note {
@ -150,23 +191,71 @@ func createTargetObjectDetails(nameText string, layout model.ObjectTypeLayout) *
return details
}
// reassignSubtreeIds makes a copy of a subtree of blocks and assign a new id for each block
func reassignSubtreeIds(rootId string, blocks []simple.Block) (string, []simple.Block) {
res := make([]simple.Block, 0, len(blocks))
mapping := map[string]string{}
for _, b := range blocks {
newId := bson.NewObjectId().Hex()
mapping[b.Model().Id] = newId
// copySubtreeOfBlocks makes a copy of a subtree of blocks and assign a new id for each block
func copySubtreeOfBlocks(s *state.State, oldRootId string, oldBlocks []simple.Block) (string, []simple.Block) {
copiedBlocks := make([]simple.Block, 0, len(oldBlocks))
oldToNewIds := map[string]string{}
newProcessedIds := map[string]struct{}{}
newBlock := b.Copy()
newBlock.Model().Id = newId
res = append(res, newBlock)
}
// duplicate blocks that can be duplicated
for _, oldBlock := range oldBlocks {
if d, ok := oldBlock.(duplicatable); ok {
newRootId, oldVisitedIds, newBlocks, err := d.Duplicate(s)
if err != nil {
log.Errorf("failed to perform newProcessedIds duplicate: %v", err)
continue
}
for _, b := range res {
for i, id := range b.Model().ChildrenIds {
b.Model().ChildrenIds[i] = mapping[id]
for _, newBlock := range newBlocks {
copiedBlocks = append(copiedBlocks, newBlock)
newProcessedIds[newBlock.Model().Id] = struct{}{}
}
for _, id := range oldVisitedIds {
// mark id as visited and already set
oldToNewIds[id] = ""
}
oldToNewIds[oldBlock.Model().Id] = newRootId
}
}
return mapping[rootId], res
// copy blocks that can't be duplicated
for _, oldBlock := range oldBlocks {
_, found := oldToNewIds[oldBlock.Model().Id]
if found {
continue
}
newId := bson.NewObjectId().Hex()
oldToNewIds[oldBlock.Model().Id] = newId
newBlock := oldBlock.Copy()
newBlock.Model().Id = newId
copiedBlocks = append(copiedBlocks, newBlock)
}
// update children ids for copied blocks
for _, copiedBlock := range copiedBlocks {
if _, hasCorrectChildren := newProcessedIds[copiedBlock.Model().Id]; hasCorrectChildren {
continue
}
for i, id := range copiedBlock.Model().ChildrenIds {
newChildId := oldToNewIds[id]
if newChildId == "" {
log.With("old id", id).
With("parent new id", copiedBlock.Model().Id).
With("parent old id", oldToNewIds[copiedBlock.Model().Id]).
Warn("empty id is set as new")
}
copiedBlock.Model().ChildrenIds[i] = newChildId
}
}
return oldToNewIds[oldRootId], copiedBlocks
}
func hasNoteLayout(s *state.State) bool {
return model.ObjectTypeLayout(pbtypes.GetInt64(s.Details(), bundle.RelationKeyLayout.String())) == model.ObjectType_note
}

View file

@ -11,8 +11,10 @@ import (
"go.uber.org/mock/gomock"
"github.com/anyproto/anytype-heart/core/block/editor/converter"
"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/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/editor/table"
"github.com/anyproto/anytype-heart/core/block/editor/template"
"github.com/anyproto/anytype-heart/core/block/simple"
"github.com/anyproto/anytype-heart/core/domain"
@ -53,24 +55,28 @@ func (tts testTemplateService) AddTemplate(id string, st *state.State) {
tts.templates[id] = st
}
func (tts testTemplateService) CreateTemplateStateWithDetails(id string, details *types.Struct) (*state.State, error) {
func (tts testTemplateService) CreateTemplateStateWithDetails(id string, details *types.Struct) (st *state.State, err error) {
if id == "" {
st := state.NewDoc("", nil).NewState()
st = state.NewDoc("", nil).NewState()
template.InitTemplate(st, template.WithEmpty,
template.WithDefaultFeaturedRelations,
template.WithFeaturedRelations,
template.WithRequiredRelations(),
template.WithRequiredRelations,
template.WithTitle,
)
return st, nil
} else {
st = tts.templates[id]
}
st := tts.templates[id]
templateDetails := st.Details()
newDetails := pbtypes.StructMerge(templateDetails, details, false)
st.SetDetails(newDetails)
return st, nil
}
func (tts testTemplateService) CreateTemplateStateFromSmartBlock(sb smartblock.SmartBlock, details *types.Struct) *state.State {
return tts.templates[sb.Id()]
}
func assertNoCommonElements(t *testing.T, a, b []string) {
got := slice.Difference(a, b)
@ -113,9 +119,10 @@ func assertDetails(t *testing.T, id string, ts testCreator, details *types.Struc
}
func TestExtractObjects(t *testing.T) {
objectId := "test"
makeTestObject := func() *smarttest.SmartTest {
sb := smarttest.New("test")
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"1", "2", "3"}}))
sb := smarttest.New(objectId)
sb.AddBlock(simple.New(&model.Block{Id: objectId, ChildrenIds: []string{"1", "2", "3"}}))
sb.AddBlock(newTextBlock("1", "text 1", []string{"1.1", "1.2"}))
sb.AddBlock(newTextBlock("1.1", "text 1.1", []string{"1.1.1"}))
sb.AddBlock(newTextBlock("1.1.1", "text 1.1.1", nil))
@ -135,9 +142,9 @@ func TestExtractObjects(t *testing.T) {
{Key: bundle.RelationKeyCoverId.String(), Value: pbtypes.String("poster with Van Damme")},
}
makeTemplateState := func() *state.State {
sb := smarttest.New("template")
sb.AddBlock(simple.New(&model.Block{Id: "template", ChildrenIds: []string{"A", "B"}}))
makeTemplateState := func(id string) *state.State {
sb := smarttest.New(id)
sb.AddBlock(simple.New(&model.Block{Id: id, ChildrenIds: []string{"A", "B"}}))
sb.AddBlock(newTextBlock("A", "text A", nil))
sb.AddBlock(newTextBlock("B", "text B", []string{"B.1"}))
sb.AddBlock(newTextBlock("B.1", "text B.1", nil))
@ -149,6 +156,7 @@ func TestExtractObjects(t *testing.T) {
for _, tc := range []struct {
name string
blockIds []string
typeKey string
templateId string
wantObjectsWithTexts [][]string
wantDetails *types.Struct
@ -270,6 +278,22 @@ func TestExtractObjects(t *testing.T) {
bundle.RelationKeyCoverId.String(): pbtypes.String("poster with Van Damme"),
}},
},
{
name: "if target layout includes title, root is not added",
blockIds: []string{"1.1"},
typeKey: bundle.TypeKeyTask.String(),
wantObjectsWithTexts: [][]string{{"text 1.1.1"}},
wantDetails: &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String("1.1"),
}},
},
{
name: "template and source are the same objects",
blockIds: []string{"1.1"},
typeKey: bundle.TypeKeyTask.String(),
templateId: objectId,
wantObjectsWithTexts: [][]string{{"text 1.1.1", "text 2.1", "text 3.1"}},
},
} {
t.Run(tc.name, func(t *testing.T) {
fixture := newFixture(t)
@ -280,20 +304,29 @@ func TestExtractObjects(t *testing.T) {
creator.Add(sb)
ts := testTemplateService{templates: map[string]*state.State{}}
tmpl := makeTemplateState()
ts.AddTemplate("template", tmpl)
var tmpl *state.State
if tc.templateId == objectId {
tmpl = sb.NewState()
} else {
tmpl = makeTemplateState(tc.templateId)
}
ts.AddTemplate(tc.templateId, tmpl)
if tc.typeKey == "" {
tc.typeKey = bundle.TypeKeyNote.String()
}
req := pb.RpcBlockListConvertToObjectsRequest{
ContextId: "test",
BlockIds: tc.blockIds,
TemplateId: tc.templateId,
ObjectTypeUniqueKey: domain.MustUniqueKey(coresb.SmartBlockTypeObjectType, bundle.TypeKeyNote.String()).Marshal(),
ObjectTypeUniqueKey: domain.MustUniqueKey(coresb.SmartBlockTypeObjectType, tc.typeKey).Marshal(),
}
ctx := session.NewContext()
linkIds, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter()).ExtractBlocksToObjects(ctx, creator, ts, req)
linkIds, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
assert.NoError(t, err)
var gotBlockIds []string
gotBlockIds := []string{}
for _, b := range sb.Blocks() {
gotBlockIds = append(gotBlockIds, b.Id)
}
@ -323,6 +356,261 @@ func TestExtractObjects(t *testing.T) {
assert.Contains(t, fields, bundle.RelationKeyName.String())
})
t.Run("add custom link block", func(t *testing.T) {
fixture := newFixture(t)
defer fixture.cleanUp()
creator := testCreator{objects: map[string]*smarttest.SmartTest{}}
sb := makeTestObject()
creator.Add(sb)
ts := testTemplateService{templates: map[string]*state.State{}}
tmpl := makeTemplateState("template")
ts.AddTemplate("template", tmpl)
req := pb.RpcBlockListConvertToObjectsRequest{
ContextId: "test",
BlockIds: []string{"1"},
ObjectTypeUniqueKey: domain.MustUniqueKey(coresb.SmartBlockTypeObjectType, bundle.TypeKeyNote.String()).Marshal(),
Block: &model.Block{Id: "newId", Content: &model.BlockContentOfLink{
Link: &model.BlockContentLink{
CardStyle: model.BlockContentLink_Card,
},
}},
}
ctx := session.NewContext()
_, 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() {
if block.GetLink() != nil {
break
}
}
assert.NotNil(t, block)
assert.Equal(t, block.GetLink().GetCardStyle(), model.BlockContentLink_Card)
})
t.Run("add custom link block for multiple blocks", func(t *testing.T) {
fixture := newFixture(t)
defer fixture.cleanUp()
creator := testCreator{objects: map[string]*smarttest.SmartTest{}}
sb := makeTestObject()
creator.Add(sb)
ts := testTemplateService{templates: map[string]*state.State{}}
tmpl := makeTemplateState("template")
ts.AddTemplate("template", tmpl)
req := pb.RpcBlockListConvertToObjectsRequest{
ContextId: "test",
BlockIds: []string{"1", "2"},
ObjectTypeUniqueKey: domain.MustUniqueKey(coresb.SmartBlockTypeObjectType, bundle.TypeKeyNote.String()).Marshal(),
Block: &model.Block{Id: "newId", Content: &model.BlockContentOfLink{
Link: &model.BlockContentLink{
CardStyle: model.BlockContentLink_Card,
},
}},
}
ctx := session.NewContext()
_, 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 {
for _, eventMessage := range message {
if blockAdd := eventMessage.Msg.GetBlockAdd(); blockAdd != nil {
addedBlocks = append(addedBlocks, blockAdd.Blocks...)
}
}
}
assert.Len(t, addedBlocks, 2)
assert.NotEqual(t, addedBlocks[0].Id, addedBlocks[1].Id)
assert.NotEqual(t, addedBlocks[0].GetLink().GetTargetBlockId(), addedBlocks[1].GetLink().GetTargetBlockId())
})
}
func TestBuildBlock(t *testing.T) {
const target = "target"
for _, tc := range []struct {
name string
input, output *model.Block
}{
{
name: "nil",
input: nil,
output: &model.Block{Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{
TargetBlockId: target,
Style: model.BlockContentLink_Page,
}}},
},
{
name: "link",
input: &model.Block{Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{
Style: model.BlockContentLink_Dashboard,
CardStyle: model.BlockContentLink_Card,
}}},
output: &model.Block{Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{
TargetBlockId: target,
Style: model.BlockContentLink_Dashboard,
CardStyle: model.BlockContentLink_Card,
}}},
},
{
name: "bookmark",
input: &model.Block{Content: &model.BlockContentOfBookmark{Bookmark: &model.BlockContentBookmark{
Type: model.LinkPreview_Image,
State: model.BlockContentBookmark_Fetching,
}}},
output: &model.Block{Content: &model.BlockContentOfBookmark{Bookmark: &model.BlockContentBookmark{
TargetObjectId: target,
Type: model.LinkPreview_Image,
State: model.BlockContentBookmark_Fetching,
}}},
},
{
name: "file",
input: &model.Block{Content: &model.BlockContentOfFile{File: &model.BlockContentFile{
Type: model.BlockContentFile_Image,
}}},
output: &model.Block{Content: &model.BlockContentOfFile{File: &model.BlockContentFile{
TargetObjectId: target,
Type: model.BlockContentFile_Image,
}}},
},
{
name: "dataview",
input: &model.Block{Content: &model.BlockContentOfDataview{Dataview: &model.BlockContentDataview{
IsCollection: true,
Source: []string{"ot-note"},
}}},
output: &model.Block{Content: &model.BlockContentOfDataview{Dataview: &model.BlockContentDataview{
TargetObjectId: target,
IsCollection: true,
Source: []string{"ot-note"},
}}},
},
{
name: "other",
input: &model.Block{Content: &model.BlockContentOfTableRow{TableRow: &model.BlockContentTableRow{
IsHeader: true,
}}},
output: &model.Block{Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{
TargetBlockId: target,
Style: model.BlockContentLink_Page,
}}},
},
} {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.output, buildBlock(tc.input, target))
})
}
}
func TestReassignSubtreeIds(t *testing.T) {
t.Run("plain blocks receive new ids", func(t *testing.T) {
// given
blocks := []simple.Block{
simple.New(&model.Block{Id: "text", ChildrenIds: []string{"1", "2"}}),
simple.New(&model.Block{Id: "1", ChildrenIds: []string{"1.1"}}),
simple.New(&model.Block{Id: "2"}),
simple.New(&model.Block{Id: "1.1"}),
}
s := generateState("text", blocks)
// when
newRoot, newBlocks := copySubtreeOfBlocks(s, "text", blocks)
// then
assert.Len(t, newBlocks, len(blocks))
assert.NotEqual(t, "text", newRoot)
for i := 0; i < len(blocks); i++ {
assert.NotEqual(t, blocks[i].Model().Id, newBlocks[i].Model().Id)
assert.True(t, bson.IsObjectIdHex(newBlocks[i].Model().Id))
}
})
t.Run("table blocks receive new ids", func(t *testing.T) {
// given
blocks := []simple.Block{
simple.New(&model.Block{Id: "parent", ChildrenIds: []string{"table"}}),
simple.New(&model.Block{Id: "table", ChildrenIds: []string{"cols", "rows"}, Content: &model.BlockContentOfTable{Table: &model.BlockContentTable{}}}),
simple.New(&model.Block{Id: "cols", ChildrenIds: []string{"col1", "col2"}, Content: &model.BlockContentOfLayout{Layout: &model.BlockContentLayout{Style: model.BlockContentLayout_TableColumns}}}),
simple.New(&model.Block{Id: "col1", Content: &model.BlockContentOfTableColumn{TableColumn: &model.BlockContentTableColumn{}}}),
simple.New(&model.Block{Id: "col2", Content: &model.BlockContentOfTableColumn{TableColumn: &model.BlockContentTableColumn{}}}),
simple.New(&model.Block{Id: "rows", ChildrenIds: []string{"row1", "row2"}, Content: &model.BlockContentOfLayout{Layout: &model.BlockContentLayout{Style: model.BlockContentLayout_TableRows}}}),
simple.New(&model.Block{Id: "row1", ChildrenIds: []string{"row1-col1", "row1-col2"}, Content: &model.BlockContentOfTableRow{TableRow: &model.BlockContentTableRow{}}}),
simple.New(&model.Block{Id: "row2", ChildrenIds: []string{"row2-col1", "row2-col2"}, Content: &model.BlockContentOfTableRow{TableRow: &model.BlockContentTableRow{}}}),
simple.New(&model.Block{Id: "row1-col1"}),
simple.New(&model.Block{Id: "row1-col2"}),
simple.New(&model.Block{Id: "row2-col1"}),
simple.New(&model.Block{Id: "row2-col2"}),
}
s := generateState("parent", blocks)
// when
root, newBlocks := copySubtreeOfBlocks(s, "parent", blocks)
// then
assert.Len(t, newBlocks, len(blocks))
assert.NotEqual(t, "text", root)
blocksMap := make(map[string]simple.Block, len(newBlocks))
tableId := ""
for i := 0; i < len(blocks); i++ {
nb := newBlocks[i]
assert.NotEqual(t, blocks[i].Model().Id, nb.Model().Id)
blocksMap[nb.Model().Id] = nb
if tb := nb.Model().GetTable(); tb != nil {
tableId = nb.Model().Id
}
}
require.NotEmpty(t, tableId)
newState := state.NewDoc("new", blocksMap).NewState()
tbl, err := table.NewTable(newState, tableId)
assert.NoError(t, err)
rows := tbl.RowIDs()
cols := tbl.ColumnIDs()
require.NoError(t, tbl.Iterate(func(b simple.Block, pos table.CellPosition) bool {
assert.Equal(t, pos.RowID, rows[pos.RowNumber])
assert.Equal(t, pos.ColID, cols[pos.ColNumber])
return true
}))
})
t.Run("table blocks receive plain ids in case of error on dup", func(t *testing.T) {
// given
blocks := []simple.Block{
simple.New(&model.Block{Id: "parent", ChildrenIds: []string{"table"}}),
simple.New(&model.Block{Id: "table", ChildrenIds: []string{"cols", "rows"}, Content: &model.BlockContentOfTable{Table: &model.BlockContentTable{}}}),
simple.New(&model.Block{Id: "rows", ChildrenIds: []string{}, Content: &model.BlockContentOfLayout{Layout: &model.BlockContentLayout{Style: model.BlockContentLayout_TableRows}}}),
}
s := generateState("parent", blocks)
// when
root, newBlocks := copySubtreeOfBlocks(s, "parent", blocks)
// then
assert.Len(t, newBlocks, len(blocks))
assert.NotEqual(t, "text", root)
for i := 0; i < len(blocks); i++ {
assert.NotEqual(t, blocks[i].Model().Id, newBlocks[i].Model().Id)
assert.True(t, bson.IsObjectIdHex(newBlocks[i].Model().Id))
}
})
}
func generateState(root string, blocks []simple.Block) *state.State {
mapping := make(map[string]simple.Block, len(blocks))
for _, b := range blocks {
mapping[b.Model().Id] = b
}
s := state.NewDoc(root, mapping).NewState()
s.Add(simple.New(&model.Block{Id: "root", ChildrenIds: []string{root}}))
return s
}
type fixture struct {
@ -335,14 +623,23 @@ func newFixture(t *testing.T) *fixture {
ctrl := gomock.NewController(t)
objectStore := testMock.NewMockObjectStore(ctrl)
objectTypeDetails := &model.ObjectDetails{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyLayout.String(): pbtypes.String(model.ObjectType_basic.String()),
},
},
}
objectStore.EXPECT().GetObjectByUniqueKey(gomock.Any(), gomock.Any()).Return(objectTypeDetails, nil).AnyTimes()
objectStore.EXPECT().GetObjectByUniqueKey(gomock.Any(), gomock.Any()).DoAndReturn(
func(_ string, uk domain.UniqueKey) (*model.ObjectDetails, error) {
layout := pbtypes.Int64(int64(model.ObjectType_basic))
switch uk.InternalKey() {
case "note":
layout = pbtypes.Int64(int64(model.ObjectType_note))
case "task":
layout = pbtypes.Int64(int64(model.ObjectType_todo))
}
return &model.ObjectDetails{
Details: &types.Struct{
Fields: map[string]*types.Value{
bundle.RelationKeyRecommendedLayout.String(): layout,
},
},
}, nil
}).AnyTimes()
return &fixture{
t: t,

View file

@ -383,6 +383,9 @@ func (cb *clipboard) pasteAny(
return
}
}
if f, ok := b.Content.(*model.BlockContentOfFile); ok {
cb.processFileBlock(f)
}
cb.optimizeLinks(b)
}
srcState := cb.blocksToState(req.AnySlot)
@ -583,6 +586,29 @@ func (cb *clipboard) newHTMLConverter(s *state.State) *html.HTML {
return html.NewHTMLConverter(cb.fileService, s, cb.fileObjectService)
}
func (cb *clipboard) processFileBlock(f *model.BlockContentOfFile) {
fileId, err := cb.fileObjectService.GetFileIdFromObject(f.File.TargetObjectId)
if err != nil {
log.Errorf("failed to get fileId: %v", err)
return
}
if cb.SpaceID() == fileId.SpaceId {
return
}
objectId, err := cb.fileObjectService.CreateFromImport(
domain.FullFileId{SpaceId: cb.SpaceID(), FileId: fileId.FileId},
objectorigin.ObjectOrigin{Origin: model.ObjectOrigin_clipboard},
)
if err != nil {
log.Errorf("failed to create file object: %v", err)
return
}
f.File.TargetObjectId = objectId
}
func renderText(s *state.State, ignoreStyle bool) string {
texts := make([]string, 0)
texts, _ = renderBlock(s, texts, s.RootId(), -1, 0, ignoreStyle)

View file

@ -13,6 +13,8 @@ import (
"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/block/simple"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/files/fileobject/mock_fileobject"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
@ -216,7 +218,9 @@ func checkBlockMarksDebug(t *testing.T, sb *smarttest.SmartTest, marksArr [][]*m
func newFixture(t *testing.T, sb smartblock.SmartBlock) Clipboard {
file := file.NewMockFile(t)
file.EXPECT().UploadState(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
return NewClipboard(sb, file, nil, nil, nil, nil)
fos := mock_fileobject.NewMockService(t)
fos.EXPECT().GetFileIdFromObject(mock.Anything).Return(domain.FullFileId{}, fmt.Errorf("no fileId")).Maybe()
return NewClipboard(sb, file, nil, nil, nil, fos)
}
func pasteAny(t *testing.T, sb *smarttest.SmartTest, id string, textRange model.Range, selectedBlockIds []string, blocks []*model.Block) ([]string, bool) {

View file

@ -2,12 +2,14 @@ package clipboard
import (
"errors"
"fmt"
"strconv"
"testing"
"github.com/gogo/protobuf/types"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
@ -18,6 +20,8 @@ import (
"github.com/anyproto/anytype-heart/core/block/simple"
_ "github.com/anyproto/anytype-heart/core/block/simple/base"
"github.com/anyproto/anytype-heart/core/block/simple/text"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/files/fileobject/mock_fileobject"
"github.com/anyproto/anytype-heart/core/session"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
@ -1124,7 +1128,7 @@ func TestClipboard_TitleOps(t *testing.T) {
t.Run("do not paste if Blocks restriction is set to smartblock", func(t *testing.T) {
// given
sb := smarttest.New("test")
sb.SetRestrictions(restriction.Restrictions{Object: restriction.ObjectRestrictions{model.Restrictions_Blocks}})
sb.TestRestrictions = restriction.Restrictions{Object: restriction.ObjectRestrictions{model.Restrictions_Blocks}}
cb := newFixture(t, sb)
// when
@ -1906,3 +1910,94 @@ bbb`},
})
}
}
func TestProcessFileBlock(t *testing.T) {
const (
fileObject1 = "fileObject1"
fileObject2 = "fileObject2"
space1 = "space1"
space2 = "space2"
fileId = domain.FileId("fileId")
)
sb := smarttest.New("test")
sb.SetSpaceId(space1)
t.Run("old target object id remains if space is the same", func(t *testing.T) {
// given
file := mock_fileobject.NewMockService(t)
file.EXPECT().GetFileIdFromObject(fileObject1).Return(domain.FullFileId{SpaceId: space1, FileId: fileId}, nil)
c := &clipboard{
SmartBlock: sb,
fileObjectService: file,
}
fb := &model.BlockContentOfFile{File: &model.BlockContentFile{TargetObjectId: fileObject1}}
// when
c.processFileBlock(fb)
// then
assert.Equal(t, fileObject1, fb.File.TargetObjectId)
})
t.Run("new target object id is set if space is different", func(t *testing.T) {
// given
file := mock_fileobject.NewMockService(t)
file.EXPECT().GetFileIdFromObject(fileObject1).Return(domain.FullFileId{SpaceId: space2, FileId: fileId}, nil)
file.EXPECT().CreateFromImport(domain.FullFileId{FileId: fileId, SpaceId: space1}, mock.Anything).Return(fileObject2, nil)
c := &clipboard{
SmartBlock: sb,
fileObjectService: file,
}
fb := &model.BlockContentOfFile{File: &model.BlockContentFile{TargetObjectId: fileObject1}}
// when
c.processFileBlock(fb)
// then
assert.Equal(t, fileObject2, fb.File.TargetObjectId)
})
t.Run("old target object id remains if failed to create new object", func(t *testing.T) {
// given
file := mock_fileobject.NewMockService(t)
file.EXPECT().GetFileIdFromObject(fileObject1).Return(domain.FullFileId{SpaceId: space2, FileId: fileId}, nil)
file.EXPECT().CreateFromImport(domain.FullFileId{FileId: fileId, SpaceId: space1}, mock.Anything).Return("", fmt.Errorf("some error"))
c := &clipboard{
SmartBlock: sb,
fileObjectService: file,
}
fb := &model.BlockContentOfFile{File: &model.BlockContentFile{TargetObjectId: fileObject1}}
// when
c.processFileBlock(fb)
// then
assert.Equal(t, fileObject1, fb.File.TargetObjectId)
})
t.Run("old target object id remains if failed to get file id", func(t *testing.T) {
// given
file := mock_fileobject.NewMockService(t)
file.EXPECT().GetFileIdFromObject(fileObject1).Return(domain.FullFileId{}, fmt.Errorf("not found"))
c := &clipboard{
SmartBlock: sb,
fileObjectService: file,
}
fb := &model.BlockContentOfFile{File: &model.BlockContentFile{TargetObjectId: fileObject1}}
// when
c.processFileBlock(fb)
// then
assert.Equal(t, fileObject1, fb.File.TargetObjectId)
})
}

View file

@ -137,7 +137,7 @@ func (c *layoutConverter) fromNoteToSet(space smartblock.Space, st *state.State)
func (c *layoutConverter) fromAnyToSet(space smartblock.Space, st *state.State) error {
source := pbtypes.GetStringList(st.Details(), bundle.RelationKeySetOf.String())
if len(source) == 0 {
if len(source) == 0 && space != nil {
defaultTypeID, err := space.GetTypeIdByKey(context.Background(), DefaultSetSource)
if err != nil {
return fmt.Errorf("get default type id: %w", err)

View file

@ -19,6 +19,9 @@ import (
"github.com/anyproto/anytype-heart/util/slice"
)
// required relations for archive beside the bundle.RequiredInternalRelations
var dashboardRequiredRelations = []domain.RelationKey{}
type Dashboard struct {
smartblock.SmartBlock
basic.AllOperations
@ -30,13 +33,14 @@ type Dashboard struct {
func NewDashboard(sb smartblock.SmartBlock, objectStore objectstore.ObjectStore, layoutConverter converter.LayoutConverter) *Dashboard {
return &Dashboard{
SmartBlock: sb,
AllOperations: basic.NewBasic(sb, objectStore, layoutConverter),
AllOperations: basic.NewBasic(sb, objectStore, layoutConverter, nil),
Collection: collection.NewCollection(sb, objectStore),
objectStore: objectStore,
}
}
func (p *Dashboard) Init(ctx *smartblock.InitContext) (err error) {
ctx.RequiredInternalRelationKeys = append(ctx.RequiredInternalRelationKeys, dashboardRequiredRelations...)
if err = p.SmartBlock.Init(ctx); err != nil {
return
}
@ -55,7 +59,6 @@ func (p *Dashboard) CreationStateMigration(ctx *smartblock.InitContext) migratio
template.WithEmpty,
template.WithDetailName("Home"),
template.WithDetailIconEmoji("🏠"),
template.WithRequiredRelations(),
template.WithNoDuplicateLinks(),
)
},

View file

@ -2,8 +2,10 @@ package dataview
import (
"context"
"errors"
"fmt"
anystore "github.com/anyproto/any-store"
"github.com/globalsign/mgo/bson"
"github.com/google/uuid"
@ -21,7 +23,6 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/badgerhelper"
"github.com/anyproto/anytype-heart/util/pbtypes"
"github.com/anyproto/anytype-heart/util/slice"
)
@ -428,7 +429,7 @@ func (d *sdataview) checkDVBlocks(info smartblock.ApplyInfo) (err error) {
func (d *sdataview) injectActiveViews(info smartblock.ApplyInfo) (err error) {
s := info.State
views, err := d.objectStore.GetActiveViews(d.Id())
if badgerhelper.IsNotFound(err) {
if errors.Is(err, anystore.ErrDocNotFound) {
return nil
}
if err != nil {

View file

@ -5,7 +5,7 @@ import (
"fmt"
"testing"
"github.com/dgraph-io/badger/v4"
anystore "github.com/anyproto/any-store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@ -128,7 +128,7 @@ func TestInjectActiveView(t *testing.T) {
fx := newFixture(t)
fx.store.EXPECT().GetActiveViews(mock.Anything).RunAndReturn(func(id string) (map[string]string, error) {
assert.Equal(t, objId, id)
return nil, badger.ErrKeyNotFound
return nil, anystore.ErrDocNotFound
})
info := getInfo()

View file

@ -0,0 +1,30 @@
package editor
import (
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/domain"
)
// required relations for device beside the bundle.RequiredInternalRelations
var deviceRequiredRelations = []domain.RelationKey{}
type DevicesObject struct {
smartblock.SmartBlock
deviceService deviceService
}
func NewDevicesObject(sb smartblock.SmartBlock, deviceService deviceService) *DevicesObject {
return &DevicesObject{
SmartBlock: sb,
deviceService: deviceService,
}
}
func (d *DevicesObject) Init(ctx *smartblock.InitContext) (err error) {
ctx.RequiredInternalRelationKeys = append(ctx.RequiredInternalRelationKeys, deviceRequiredRelations...)
if err = d.SmartBlock.Init(ctx); err != nil {
return
}
d.AddHook(d.deviceService.SaveDeviceInfo, smartblock.HookAfterApply)
return nil
}

View file

@ -40,6 +40,10 @@ type accountService interface {
MyParticipantId(spaceId string) string
}
type deviceService interface {
SaveDeviceInfo(info smartblock.ApplyInfo) error
}
type ObjectFactory struct {
bookmarkService bookmark.BookmarkService
fileBlockService file.BlockService
@ -61,6 +65,7 @@ type ObjectFactory struct {
fileUploaderService fileuploader.Service
fileReconciler reconciler.Reconciler
objectDeleter ObjectDeleter
deviceService deviceService
}
func NewObjectFactory() *ObjectFactory {
@ -88,6 +93,7 @@ func (f *ObjectFactory) Init(a *app.App) (err error) {
f.fileUploaderService = app.MustComponent[fileuploader.Service](a)
f.objectDeleter = app.MustComponent[ObjectDeleter](a)
f.fileReconciler = app.MustComponent[reconciler.Reconciler](a)
f.deviceService = app.MustComponent[deviceService](a)
return nil
}
@ -185,6 +191,8 @@ func (f *ObjectFactory) New(space smartblock.Space, sbType coresb.SmartBlockType
return nil, fmt.Errorf("subobject not supported via factory")
case coresb.SmartBlockTypeParticipant:
return f.newParticipant(sb), nil
case coresb.SmartBlockTypeDevicesObject:
return NewDevicesObject(sb, f.deviceService), nil
default:
return nil, fmt.Errorf("unexpected smartblock type: %v", sbType)
}

View file

@ -110,7 +110,7 @@ func TestDropFiles(t *testing.T) {
t.Run("do not drop files to object with Blocks restriction", func(t *testing.T) {
// given
fx := newFixture(t)
fx.sb.SetRestrictions(restriction.Restrictions{Object: restriction.ObjectRestrictions{model.Restrictions_Blocks}})
fx.sb.TestRestrictions = restriction.Restrictions{Object: restriction.ObjectRestrictions{model.Restrictions_Blocks}}
// when
err := fx.sfile.DropFiles(pb.RpcFileDropRequest{})

View file

@ -14,11 +14,18 @@ import (
"github.com/anyproto/anytype-heart/core/files/fileobject"
"github.com/anyproto/anytype-heart/core/files/reconciler"
"github.com/anyproto/anytype-heart/core/filestorage"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
coresb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
)
// required relations for files beside the bundle.RequiredInternalRelations
var fileRequiredRelations = append(pageRequiredRelations, []domain.RelationKey{
bundle.RelationKeyFileBackupStatus,
bundle.RelationKeyFileSyncStatus,
}...)
func (f *ObjectFactory) newFile(sb smartblock.SmartBlock) *File {
basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter)
basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService)
return &File{
SmartBlock: sb,
ChangeReceiver: sb.(source.ChangeReceiver),
@ -65,6 +72,8 @@ func (f *File) Init(ctx *smartblock.InitContext) error {
return fmt.Errorf("source type should be a file")
}
ctx.RequiredInternalRelationKeys = append(ctx.RequiredInternalRelationKeys, fileRequiredRelations...)
if ctx.BuildOpts.DisableRemoteLoad {
ctx.Ctx = context.WithValue(ctx.Ctx, filestorage.CtxKeyRemoteLoadDisabled, true)
}

View file

@ -5,12 +5,16 @@ import (
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/editor/template"
"github.com/anyproto/anytype-heart/core/block/migration"
"github.com/anyproto/anytype-heart/core/domain"
)
type NotificationObject struct {
smartblock.SmartBlock
}
// required relations for notifications beside the bundle.RequiredInternalRelations
var notificationsRequiredRelations = []domain.RelationKey{}
func NewNotificationObject(sb smartblock.SmartBlock) *NotificationObject {
return &NotificationObject{
SmartBlock: sb,
@ -30,6 +34,7 @@ func (n *NotificationObject) CreationStateMigration(ctx *smartblock.InitContext)
}
func (n *NotificationObject) Init(ctx *smartblock.InitContext) (err error) {
ctx.RequiredInternalRelationKeys = notificationsRequiredRelations
if err = n.SmartBlock.Init(ctx); err != nil {
return
}

View file

@ -23,6 +23,18 @@ import (
"github.com/anyproto/anytype-heart/util/pbtypes"
)
var pageRequiredRelations = []domain.RelationKey{
bundle.RelationKeyCoverId,
bundle.RelationKeyCoverScale,
bundle.RelationKeyCoverType,
bundle.RelationKeyCoverX,
bundle.RelationKeyCoverY,
bundle.RelationKeySnippet,
bundle.RelationKeyFeaturedRelations,
bundle.RelationKeyLinks,
bundle.RelationKeyLayoutAlign,
}
type Page struct {
smartblock.SmartBlock
basic.AllOperations
@ -46,7 +58,7 @@ func (f *ObjectFactory) newPage(sb smartblock.SmartBlock) *Page {
return &Page{
SmartBlock: sb,
ChangeReceiver: sb.(source.ChangeReceiver),
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter),
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService),
IHistory: basic.NewHistory(sb),
Text: stext.NewText(
sb,
@ -72,6 +84,7 @@ func (f *ObjectFactory) newPage(sb smartblock.SmartBlock) *Page {
}
func (p *Page) Init(ctx *smartblock.InitContext) (err error) {
ctx.RequiredInternalRelationKeys = append(ctx.RequiredInternalRelationKeys, pageRequiredRelations...)
if ctx.ObjectTypeKeys == nil && (ctx.State == nil || len(ctx.State.ObjectTypeKeys()) == 0) && ctx.IsNewObject {
ctx.ObjectTypeKeys = []domain.TypeKey{bundle.TypeKeyPage}
}
@ -162,7 +175,6 @@ func (p *Page) CreationStateMigration(ctx *smartblock.InitContext) migration.Mig
template.WithLayout(layout),
template.WithDefaultFeaturedRelations,
template.WithFeaturedRelations,
template.WithRequiredRelations(),
template.WithLinkFieldsMigration,
template.WithCreatorRemovedFromFeaturedRelations,
}
@ -171,8 +183,8 @@ func (p *Page) CreationStateMigration(ctx *smartblock.InitContext) migration.Mig
case model.ObjectType_note:
templates = append(templates,
template.WithNameToFirstBlock,
template.WithFirstTextBlock,
template.WithNoTitle,
template.WithNoDescription,
)
case model.ObjectType_todo:
templates = append(templates,

View file

@ -8,19 +8,30 @@ import (
"github.com/anyproto/anytype-heart/core/block/editor/basic"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/editor/template"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/space/spaceinfo"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
var participantRequiredRelations = []domain.RelationKey{
bundle.RelationKeyGlobalName,
bundle.RelationKeyIdentity,
bundle.RelationKeyBacklinks,
bundle.RelationKeyParticipantPermissions,
bundle.RelationKeyParticipantStatus,
bundle.RelationKeyIdentityProfileLink,
bundle.RelationKeyIsHiddenDiscovery,
}
type participant struct {
smartblock.SmartBlock
basic.DetailsUpdatable
}
func (f *ObjectFactory) newParticipant(sb smartblock.SmartBlock) *participant {
basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter)
basicComponent := basic.NewBasic(sb, f.objectStore, f.layoutConverter, nil)
return &participant{
SmartBlock: sb,
DetailsUpdatable: basicComponent,
@ -29,6 +40,7 @@ func (f *ObjectFactory) newParticipant(sb smartblock.SmartBlock) *participant {
func (p *participant) Init(ctx *smartblock.InitContext) (err error) {
// Details come from aclobjectmanager, see buildParticipantDetails
ctx.RequiredInternalRelationKeys = append(ctx.RequiredInternalRelationKeys, participantRequiredRelations...)
if err = p.SmartBlock.Init(ctx); err != nil {
return

View file

@ -135,7 +135,7 @@ func newStoreFixture(t *testing.T) *objectstore.StoreFixture {
func newParticipantTest(t *testing.T) (*participant, error) {
sb := smarttest.New("root")
store := newStoreFixture(t)
basicComponent := basic.NewBasic(sb, store, nil)
basicComponent := basic.NewBasic(sb, store, nil, nil)
p := &participant{
SmartBlock: sb,
DetailsUpdatable: basicComponent,

View file

@ -40,7 +40,7 @@ func (f *ObjectFactory) newProfile(sb smartblock.SmartBlock) *Profile {
fileComponent := file.NewFile(sb, f.fileBlockService, f.picker, f.processService, f.fileUploaderService)
return &Profile{
SmartBlock: sb,
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter),
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService),
IHistory: basic.NewHistory(sb),
Text: stext.NewText(
sb,
@ -82,7 +82,6 @@ func (p *Profile) CreationStateMigration(ctx *smartblock.InitContext) migration.
template.InitTemplate(st,
template.WithObjectTypesAndLayout([]domain.TypeKey{bundle.TypeKeyProfile}, model.ObjectType_profile),
template.WithDetail(bundle.RelationKeyLayoutAlign, pbtypes.Float64(float64(model.Block_AlignCenter))),
template.WithRequiredRelations(),
migrationSetHidden,
)
},

View file

@ -29,8 +29,6 @@ func (sb *smartBlock) updateBackLinks(s *state.State) {
func (sb *smartBlock) injectLinksDetails(s *state.State) {
links := sb.navigationalLinks(s)
links = slice.RemoveMut(links, sb.Id())
// todo: we need to move it to the injectDerivedDetails, but we don't call it now on apply
s.SetLocalDetail(bundle.RelationKeyLinks.String(), pbtypes.StringList(links))
}

View file

@ -41,6 +41,7 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/pkg/lib/threads"
"github.com/anyproto/anytype-heart/util/anonymize"
"github.com/anyproto/anytype-heart/util/internalflag"
"github.com/anyproto/anytype-heart/util/pbtypes"
"github.com/anyproto/anytype-heart/util/slice"
@ -128,6 +129,8 @@ type Space interface {
Do(objectId string, apply func(sb SmartBlock) error) error
DoLockedIfNotExists(objectID string, proc func() error) error // TODO Temporarily before rewriting favorites/archive mechanism
StoredIds() []string
}
type SmartBlock interface {
@ -160,7 +163,6 @@ type SmartBlock interface {
CheckSubscriptions() (changed bool)
GetDocInfo() DocInfo
Restrictions() restriction.Restrictions
SetRestrictions(r restriction.Restrictions)
ObjectClose(ctx session.Context)
ObjectCloseAllSessions()
@ -186,17 +188,18 @@ type DocInfo struct {
// TODO Maybe create constructor? Don't want to forget required fields
type InitContext struct {
IsNewObject bool
Source source.Source
ObjectTypeKeys []domain.TypeKey
RelationKeys []string
State *state.State
Relations []*model.Relation
Restriction restriction.Service
ObjectStore objectstore.ObjectStore
SpaceID string
BuildOpts source.BuildOptions
Ctx context.Context
IsNewObject bool
Source source.Source
ObjectTypeKeys []domain.TypeKey
RelationKeys []string
RequiredInternalRelationKeys []domain.RelationKey // bundled relations that MUST be present in the state
State *state.State
Relations []*model.Relation
Restriction restriction.Service
ObjectStore objectstore.ObjectStore
SpaceID string
BuildOpts source.BuildOptions
Ctx context.Context
}
type linkSource interface {
@ -308,6 +311,7 @@ func (sb *smartBlock) ObjectTypeID() string {
}
func (sb *smartBlock) Init(ctx *InitContext) (err error) {
ctx.RequiredInternalRelationKeys = append(ctx.RequiredInternalRelationKeys, bundle.RequiredInternalRelations...)
if sb.Doc, err = ctx.Source.ReadDoc(ctx.Ctx, sb, ctx.State != nil); err != nil {
return fmt.Errorf("reading document: %w", err)
}
@ -335,18 +339,24 @@ func (sb *smartBlock) Init(ctx *InitContext) (err error) {
ctx.State.SetParent(sb.Doc.(*state.State))
}
injectRequiredRelationLinks := func(s *state.State) {
s.AddBundledRelationLinks(bundle.RequiredInternalRelations...)
s.AddBundledRelationLinks(ctx.RequiredInternalRelationKeys...)
}
injectRequiredRelationLinks(ctx.State)
injectRequiredRelationLinks(ctx.State.ParentState())
if err = sb.AddRelationLinksToState(ctx.State, ctx.RelationKeys...); err != nil {
return
}
// Add bundled relations
var relKeys []domain.RelationKey
for k := range ctx.State.Details().GetFields() {
if _, err := bundle.GetRelation(domain.RelationKey(k)); err == nil {
if bundle.HasRelation(k) {
relKeys = append(relKeys, domain.RelationKey(k))
}
}
ctx.State.AddBundledRelations(relKeys...)
ctx.State.AddBundledRelationLinks(relKeys...)
if ctx.IsNewObject && ctx.State != nil {
source.NewSubObjectsAndProfileLinksMigration(sb.Type(), sb.space, sb.currentParticipantId, sb.objectStore).Migrate(ctx.State)
}
@ -376,11 +386,7 @@ func (sb *smartBlock) sendObjectCloseEvent(_ ApplyInfo) error {
// updateRestrictions refetch restrictions from restriction service and update them in the smartblock
func (sb *smartBlock) updateRestrictions() {
restrictions := sb.restrictionService.GetRestrictions(sb)
sb.SetRestrictions(restrictions)
}
func (sb *smartBlock) SetRestrictions(r restriction.Restrictions) {
r := sb.restrictionService.GetRestrictions(sb)
if sb.restrictions.Equal(r) {
return
}
@ -447,7 +453,7 @@ func (sb *smartBlock) fetchMeta() (details []*model.ObjectViewDetailsSet, err er
recordsCh := make(chan *types.Struct, 10)
sb.recordsSub = database.NewSubscription(nil, recordsCh)
depIDs := sb.dependentSmartIds(sb.includeRelationObjectsAsDependents, true, true, true)
depIDs := sb.dependentSmartIds(sb.includeRelationObjectsAsDependents, true, true)
sb.setDependentIDs(depIDs)
var records []database.Record
@ -538,7 +544,7 @@ func (sb *smartBlock) onMetaChange(details *types.Struct) {
}
// dependentSmartIds returns list of dependent objects in this order: Simple blocks(Link, mentions in Text), Relations. Both of them are returned in the order of original blocks/relations
func (sb *smartBlock) dependentSmartIds(includeRelations, includeObjTypes, includeCreatorModifier, _ bool) (ids []string) {
func (sb *smartBlock) dependentSmartIds(includeRelations, includeObjTypes, includeCreatorModifier bool) (ids []string) {
return objectlink.DependentObjectIDs(sb.Doc.(*state.State), sb.Space(), true, true, includeRelations, includeObjTypes, includeCreatorModifier)
}
@ -630,8 +636,6 @@ func (sb *smartBlock) Apply(s *state.State, flags ...ApplyFlag) (err error) {
}
}
sb.beforeStateApply(s)
if !keepInternalFlags {
removeInternalFlags(s)
}
@ -706,7 +710,7 @@ func (sb *smartBlock) Apply(s *state.State, flags ...ApplyFlag) (err error) {
if !act.IsEmpty() {
if len(changes) == 0 && !doSnapshot {
log.Errorf("apply 0 changes %s: %v", st.RootId(), msgs)
log.Errorf("apply 0 changes %s: %v", st.RootId(), anonymize.Events(msgsToEvents(msgs)))
}
err = pushChange()
if err != nil {
@ -718,7 +722,7 @@ func (sb *smartBlock) Apply(s *state.State, flags ...ApplyFlag) (err error) {
sb.undo.Add(act)
}
}
} else if hasChanges(changes) || migrationVersionUpdated { // TODO: change to len(changes) > 0
} else if hasChangesToPush(changes) || migrationVersionUpdated { // TODO: change to len(changes) > 0
// log.Errorf("sb apply %s: store changes %s", sb.Id(), pbtypes.Sprint(&pb.Change{Content: changes}))
err = pushChange()
if err != nil {
@ -775,7 +779,6 @@ func (sb *smartBlock) ResetToVersion(s *state.State) (err error) {
s.SetParent(sb.Doc.(*state.State))
sb.storeFileKeys(s)
sb.injectLocalDetails(s)
sb.injectDerivedDetails(s, sb.SpaceID(), sb.Type())
if err = sb.Apply(s, NoHistory, DoSnapshot, NoRestrictions); err != nil {
return
}
@ -786,7 +789,7 @@ func (sb *smartBlock) ResetToVersion(s *state.State) (err error) {
}
func (sb *smartBlock) CheckSubscriptions() (changed bool) {
depIDs := sb.dependentSmartIds(sb.includeRelationObjectsAsDependents, true, true, true)
depIDs := sb.dependentSmartIds(sb.includeRelationObjectsAsDependents, true, true)
changed = sb.setDependentIDs(depIDs)
if sb.recordsSub == nil {
@ -845,6 +848,8 @@ func (sb *smartBlock) AddRelationLinksToState(s *state.State, relationKeys ...st
if len(relationKeys) == 0 {
return
}
// todo: filter-out existing relation links?
// in the most cases it should save as an objectstore query
relations, err := sb.objectStore.FetchRelationByKeys(sb.SpaceID(), relationKeys...)
if err != nil {
return
@ -1284,11 +1289,6 @@ func (sb *smartBlock) runIndexer(s *state.State, opts ...IndexOption) {
}
}
func (sb *smartBlock) beforeStateApply(s *state.State) {
sb.setRestrictionsDetail(s)
sb.injectLinksDetails(s)
}
func removeInternalFlags(s *state.State) {
flags := internalflag.NewFromState(s)
@ -1335,21 +1335,23 @@ func ObjectApplyTemplate(sb SmartBlock, s *state.State, templates ...template.St
return sb.Apply(s, NoHistory, NoEvent, NoRestrictions, SkipIfNoChanges)
}
func hasChanges(changes []*pb.ChangeContent) bool {
func hasChangesToPush(changes []*pb.ChangeContent) bool {
for _, ch := range changes {
if isStoreOrNotificationChanges(ch) {
if isSuitableChanges(ch) {
return true
}
}
return false
}
func isStoreOrNotificationChanges(ch *pb.ChangeContent) bool {
func isSuitableChanges(ch *pb.ChangeContent) bool {
return ch.GetStoreKeySet() != nil ||
ch.GetStoreKeyUnset() != nil ||
ch.GetStoreSliceUpdate() != nil ||
ch.GetNotificationCreate() != nil ||
ch.GetNotificationUpdate() != nil
ch.GetNotificationUpdate() != nil ||
ch.GetDeviceUpdate() != nil ||
ch.GetDeviceAdd() != nil
}
func hasDetailsMsgs(msgs []simple.EventMessage) bool {
@ -1447,6 +1449,7 @@ func (sb *smartBlock) injectDerivedDetails(s *state.State, spaceID string, sbt s
s.SetDetailAndBundledRelation(bundle.RelationKeyIsDeleted, pbtypes.Bool(isDeleted))
}
sb.injectLinksDetails(s)
sb.updateBackLinks(s)
}

View file

@ -43,8 +43,14 @@ func TestSmartBlock_Init(t *testing.T) {
fx.store.EXPECT().UpdatePendingLocalDetails(mock.Anything, mock.Anything).Return(nil).Maybe()
// when
fx.init(t, []*model.Block{{Id: id}})
initCtx := fx.init(t, []*model.Block{{Id: id}})
require.NotNil(t, initCtx)
require.NotNil(t, initCtx.State)
links := initCtx.State.GetRelationLinks()
for _, key := range bundle.RequiredInternalRelations {
assert.Truef(t, links.Has(key.String()), "missing relation %s", key)
}
// then
assert.Equal(t, id, fx.RootId())
}
@ -460,9 +466,36 @@ func TestInjectLocalDetails(t *testing.T) {
assert.Equal(t, fx.source.creator, pbtypes.GetString(st.LocalDetails(), bundle.RelationKeyCreator.String()))
assert.Equal(t, fx.source.createdDate, pbtypes.GetInt64(st.LocalDetails(), bundle.RelationKeyCreatedDate.String()))
})
// TODO More tests
}
func TestInjectDerivedDetails(t *testing.T) {
const (
id = "id"
spaceId = "testSpace"
)
t.Run("links are updated on injection", func(t *testing.T) {
// given
fx := newFixture(id, t)
fx.store.EXPECT().GetInboundLinksByID(id).Return(nil, nil)
st := state.NewDoc("id", map[string]simple.Block{
id: simple.New(&model.Block{Id: id, ChildrenIds: []string{"dataview", "link"}}),
"dataview": simple.New(&model.Block{Id: "dataview", Content: &model.BlockContentOfDataview{Dataview: &model.BlockContentDataview{TargetObjectId: "some_set"}}}),
"link": simple.New(&model.Block{Id: "link", Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{TargetBlockId: "some_obj"}}}),
}).NewState()
st.AddRelationLinks(&model.RelationLink{Key: bundle.RelationKeyAssignee.String(), Format: model.RelationFormat_object})
st.SetDetail(bundle.RelationKeyAssignee.String(), pbtypes.String("Kirill"))
// when
fx.injectDerivedDetails(st, spaceId, smartblock.SmartBlockTypePage)
// then
assert.Len(t, pbtypes.GetStringList(st.LocalDetails(), bundle.RelationKeyLinks.String()), 3)
})
}
type fixture struct {
store *mock_objectstore.MockObjectStore
restrictionService *mock_restriction.MockService
@ -500,7 +533,7 @@ func newFixture(id string, t *testing.T) *fixture {
}
}
func (fx *fixture) init(t *testing.T, blocks []*model.Block) {
func (fx *fixture) init(t *testing.T, blocks []*model.Block) *InitContext {
bm := make(map[string]simple.Block)
for _, b := range blocks {
bm[b.Id] = simple.New(b)
@ -508,12 +541,14 @@ func (fx *fixture) init(t *testing.T, blocks []*model.Block) {
doc := state.NewDoc(fx.source.id, bm)
fx.source.doc = doc
err := fx.Init(&InitContext{
initCtx := &InitContext{
Ctx: context.Background(),
SpaceID: "space1",
Source: fx.source,
})
}
err := fx.Init(initCtx)
require.NoError(t, err)
return initCtx
}
type sourceStub struct {

View file

@ -118,6 +118,10 @@ func (s *stubSpace) IsPersonal() bool {
return false
}
func (s *stubSpace) StoredIds() []string {
return nil
}
func (st *SmartTest) Space() smartblock.Space {
if st.space != nil {
return st.space
@ -171,10 +175,6 @@ func (st *SmartTest) Tree() objecttree.ObjectTree {
return st.objectTree
}
func (st *SmartTest) SetRestrictions(r restriction.Restrictions) {
st.TestRestrictions = r
}
func (st *SmartTest) Restrictions() restriction.Restrictions {
return st.TestRestrictions
}

View file

@ -26,6 +26,21 @@ var spaceViewLog = logging.Logger("core.block.editor.spaceview")
var ErrIncorrectSpaceInfo = errors.New("space info is incorrect")
// required relations for spaceview beside the bundle.RequiredInternalRelations
var spaceViewRequiredRelations = []domain.RelationKey{
bundle.RelationKeySpaceLocalStatus,
bundle.RelationKeySpaceRemoteStatus,
bundle.RelationKeyTargetSpaceId,
bundle.RelationKeySpaceInviteFileCid,
bundle.RelationKeySpaceInviteFileKey,
bundle.RelationKeyIsAclShared,
bundle.RelationKeySharedSpacesLimit,
bundle.RelationKeySpaceAccountStatus,
bundle.RelationKeySpaceShareableStatus,
bundle.RelationKeySpaceAccessType,
bundle.RelationKeyLatestAclHeadId,
}
type spaceService interface {
OnViewUpdated(info spaceinfo.SpacePersistentInfo)
OnWorkspaceChanged(spaceId string, details *types.Struct)
@ -51,6 +66,7 @@ func (f *ObjectFactory) newSpaceView(sb smartblock.SmartBlock) *SpaceView {
// Init initializes SpaceView
func (s *SpaceView) Init(ctx *smartblock.InitContext) (err error) {
ctx.RequiredInternalRelationKeys = append(ctx.RequiredInternalRelationKeys, spaceViewRequiredRelations...)
if err = s.SmartBlock.Init(ctx); err != nil {
return
}
@ -95,11 +111,6 @@ func (s *SpaceView) StateMigrations() migration.Migrations {
func (s *SpaceView) initTemplate(st *state.State) {
template.InitTemplate(st,
template.WithObjectTypesAndLayout([]domain.TypeKey{bundle.TypeKeySpaceView}, model.ObjectType_spaceView),
template.WithRelations([]domain.RelationKey{
bundle.RelationKeySpaceLocalStatus,
bundle.RelationKeySpaceRemoteStatus,
bundle.RelationKeyTargetSpaceId,
}),
)
}

View file

@ -9,6 +9,7 @@ import (
"github.com/gogo/protobuf/types"
"github.com/hashicorp/go-multierror"
"github.com/mb0/diff"
"golang.org/x/exp/slices"
"github.com/anyproto/anytype-heart/core/block/simple"
"github.com/anyproto/anytype-heart/core/domain"
@ -250,6 +251,10 @@ func (s *State) applyChange(ch *pb.ChangeContent) (err error) {
s.addNotification(ch.GetNotificationCreate().GetNotification())
case ch.GetNotificationUpdate() != nil:
s.updateNotification(ch.GetNotificationUpdate())
case ch.GetDeviceAdd() != nil:
s.addDevice(ch.GetDeviceAdd().GetDevice())
case ch.GetDeviceUpdate() != nil:
s.updateDevice(ch.GetDeviceUpdate())
default:
return fmt.Errorf("unexpected changes content type: %v", ch)
}
@ -459,6 +464,23 @@ func (s *State) updateNotification(update *pb.ChangeNotificationUpdate) {
s.notifications[update.Id].Status = update.Status
}
func (s *State) addDevice(deviceInfo *model.DeviceInfo) {
if s.deviceStore == nil {
s.deviceStore = map[string]*model.DeviceInfo{}
}
s.deviceStore[deviceInfo.Id] = deviceInfo
}
func (s *State) updateDevice(update *pb.ChangeDeviceUpdate) {
if s.deviceStore == nil {
return
}
if _, ok := s.deviceStore[update.Id]; !ok {
return
}
s.deviceStore[update.Id].Name = update.Name
}
func (s *State) GetChanges() []*pb.ChangeContent {
return s.changes
}
@ -559,6 +581,8 @@ func (s *State) fillChanges(msgs []simple.EventMessage) {
updMsgs = append(updMsgs, msg.Msg)
case *pb.EventMessageValueOfBlockSetRestrictions:
updMsgs = append(updMsgs, msg.Msg)
case *pb.EventMessageValueOfBlockSetTableRow:
updMsgs = append(updMsgs, msg.Msg)
default:
log.Errorf("unexpected event - can't convert to changes: %T", msg.Msg.GetValue())
}
@ -577,22 +601,28 @@ func (s *State) fillChanges(msgs []simple.EventMessage) {
})
}
if len(newRelLinks) > 0 {
cb.AddChange(&pb.ChangeContent{
Value: &pb.ChangeContentValueOfRelationAdd{
RelationAdd: &pb.ChangeRelationAdd{
RelationLinks: newRelLinks,
filteredRelationsLinks := s.filterLocalAndDerivedRelations(newRelLinks)
if len(filteredRelationsLinks) > 0 {
cb.AddChange(&pb.ChangeContent{
Value: &pb.ChangeContentValueOfRelationAdd{
RelationAdd: &pb.ChangeRelationAdd{
RelationLinks: filteredRelationsLinks,
},
},
},
})
})
}
}
if len(delRelIds) > 0 {
cb.AddChange(&pb.ChangeContent{
Value: &pb.ChangeContentValueOfRelationRemove{
RelationRemove: &pb.ChangeRelationRemove{
RelationKey: delRelIds,
filteredRelationsKeys := s.filterLocalAndDerivedRelationsByKey(delRelIds)
if len(filteredRelationsKeys) > 0 {
cb.AddChange(&pb.ChangeContent{
Value: &pb.ChangeContentValueOfRelationRemove{
RelationRemove: &pb.ChangeRelationRemove{
RelationKey: filteredRelationsKeys,
},
},
},
})
})
}
}
if len(updMsgs) > 0 {
cb.AddChange(&pb.ChangeContent{
@ -610,6 +640,27 @@ func (s *State) fillChanges(msgs []simple.EventMessage) {
s.changes = append(s.changes, s.makeOriginalCreatedChanges()...)
s.changes = append(s.changes, s.diffFileInfo()...)
s.changes = append(s.changes, s.makeNotificationChanges()...)
s.changes = append(s.changes, s.makeDeviceInfoChanges()...)
}
func (s *State) filterLocalAndDerivedRelations(newRelLinks pbtypes.RelationLinks) pbtypes.RelationLinks {
var relLinksWithoutLocal pbtypes.RelationLinks
for _, link := range newRelLinks {
if !slices.Contains(bundle.LocalAndDerivedRelationKeys, link.Key) {
relLinksWithoutLocal = relLinksWithoutLocal.Append(link)
}
}
return relLinksWithoutLocal
}
func (s *State) filterLocalAndDerivedRelationsByKey(relationKeys []string) []string {
var relKeysWithoutLocal []string
for _, key := range relationKeys {
if !slices.Contains(bundle.LocalAndDerivedRelationKeys, key) {
relKeysWithoutLocal = append(relKeysWithoutLocal, key)
}
}
return relKeysWithoutLocal
}
func (s *State) fillStructureChanges(cb *changeBuilder, msgs []*pb.EventBlockSetChildrenIds) {
@ -823,6 +874,34 @@ func (s *State) makeNotificationChanges() []*pb.ChangeContent {
return changes
}
func (s *State) makeDeviceInfoChanges() []*pb.ChangeContent {
changes := make([]*pb.ChangeContent, 0)
for id, device := range s.deviceStore {
if s.parent != nil {
if d := s.parent.GetDevice(id); d != nil {
if device.Name != d.Name {
changes = append(changes, &pb.ChangeContent{
Value: &pb.ChangeContentValueOfDeviceUpdate{
DeviceUpdate: &pb.ChangeDeviceUpdate{
Id: device.Id,
Name: device.Name,
},
},
})
}
continue
}
}
// if parent is nil or device is absence in parent state
changes = append(changes, &pb.ChangeContent{
Value: &pb.ChangeContentValueOfDeviceAdd{
DeviceAdd: &pb.ChangeDeviceAdd{Device: device},
},
})
}
return changes
}
type dstrings struct{ a, b []string }
func (d *dstrings) Equal(i, j int) bool { return d.a[i] == d.b[j] }

View file

@ -12,6 +12,7 @@ import (
"github.com/anyproto/anytype-heart/core/block/simple/dataview"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
@ -698,6 +699,69 @@ func TestRelationChanges(t *testing.T) {
require.Equal(t, a.relationLinks, ac.relationLinks)
}
func TestLocalRelationChanges(t *testing.T) {
t.Run("local relation added", func(t *testing.T) {
// given
a := NewDoc("root", nil).(*State)
a.relationLinks = []*model.RelationLink{}
b := a.NewState()
b.relationLinks = []*model.RelationLink{{Key: bundle.RelationKeySyncStatus.String(), Format: model.RelationFormat_number}}
// when
_, _, err := ApplyState(b, false)
require.NoError(t, err)
chs := a.GetChanges()
// then
require.Len(t, chs, 0)
})
t.Run("local relation removed", func(t *testing.T) {
// given
a := NewDoc("root", nil).(*State)
a.relationLinks = []*model.RelationLink{{Key: bundle.RelationKeySyncStatus.String(), Format: model.RelationFormat_number}}
b := a.NewState()
b.relationLinks = []*model.RelationLink{}
// when
_, _, err := ApplyState(b, false)
require.NoError(t, err)
chs := a.GetChanges()
// then
require.Len(t, chs, 0)
})
t.Run("derived relation added", func(t *testing.T) {
// given
a := NewDoc("root", nil).(*State)
a.relationLinks = []*model.RelationLink{}
b := a.NewState()
b.relationLinks = []*model.RelationLink{{Key: bundle.RelationKeySpaceId.String(), Format: model.RelationFormat_longtext}}
// when
_, _, err := ApplyState(b, false)
require.NoError(t, err)
chs := a.GetChanges()
// then
require.Len(t, chs, 0)
})
t.Run("derived relation removed", func(t *testing.T) {
// given
a := NewDoc("root", nil).(*State)
a.relationLinks = []*model.RelationLink{{Key: bundle.RelationKeySpaceId.String(), Format: model.RelationFormat_longtext}}
b := a.NewState()
b.relationLinks = []*model.RelationLink{}
// when
_, _, err := ApplyState(b, false)
require.NoError(t, err)
chs := a.GetChanges()
// then
require.Len(t, chs, 0)
})
}
func TestRootBlockChanges(t *testing.T) {
a := NewDoc("root", nil).(*State)
s := a.NewState()
@ -807,3 +871,126 @@ func Test_migrateObjectTypeIDToKey(t *testing.T) {
})
}
}
func TestRootDeviceChanges(t *testing.T) {
t.Run("no changes", func(t *testing.T) {
// given
a := NewDoc("root", nil).(*State)
s := a.NewState()
// when
_, _, err := ApplyState(s, true)
// then
assert.Nil(t, err)
assert.Len(t, s.GetChanges(), 0)
})
t.Run("add new device", func(t *testing.T) {
// given
a := NewDoc("root", nil).(*State)
s := a.NewState()
device := &model.DeviceInfo{
Id: "id",
Name: "test",
}
s.AddDevice(device)
// when
_, _, err := ApplyState(s, true)
// then
assert.Nil(t, err)
assert.Len(t, s.GetChanges(), 1)
assert.Equal(t, device, s.GetChanges()[0].GetDeviceAdd().GetDevice())
})
t.Run("update device", func(t *testing.T) {
// given
a := NewDoc("root", nil).(*State)
device := &model.DeviceInfo{
Id: "id",
Name: "test",
}
a.AddDevice(device)
s := a.NewState()
s.SetDeviceName("id", "test1")
// when
_, _, err := ApplyState(s, true)
// then
assert.Nil(t, err)
assert.Len(t, s.GetChanges(), 1)
assert.Equal(t, "test1", s.GetChanges()[0].GetDeviceUpdate().GetName())
})
t.Run("add device - parent nil", func(t *testing.T) {
// given
a := NewDoc("root", nil).(*State)
s := a.NewState()
device := &model.DeviceInfo{
Id: "id",
Name: "test",
}
s.AddDevice(device)
s.parent = nil
// when
_, _, err := ApplyState(s, true)
// then
assert.Nil(t, err)
assert.Len(t, s.GetChanges(), 1)
assert.Equal(t, device, s.GetChanges()[0].GetDeviceAdd().GetDevice())
})
}
func TestTableChanges(t *testing.T) {
t.Run("change row header", func(t *testing.T) {
contRow := &model.BlockContentOfLayout{
Layout: &model.BlockContentLayout{
Style: model.BlockContentLayout_Row,
},
}
contColumn := &model.BlockContentOfLayout{
Layout: &model.BlockContentLayout{
Style: model.BlockContentLayout_Column,
},
}
r := NewDoc("root", nil).(*State)
s := r.NewState()
s.Add(simple.New(&model.Block{Id: "root", ChildrenIds: []string{"r1", "t1"}}))
s.Add(simple.New(&model.Block{Id: "r1", ChildrenIds: []string{"c1", "c2"}, Content: contRow}))
s.Add(simple.New(&model.Block{Id: "c1", Content: contColumn}))
s.Add(simple.New(&model.Block{Id: "c2", Content: contColumn}))
s.Add(simple.New(&model.Block{Id: "t1", ChildrenIds: []string{"tableRows", "tableColumns"}, Content: &model.BlockContentOfTable{
Table: &model.BlockContentTable{},
}}))
s.Add(simple.New(&model.Block{Id: "tableRows", ChildrenIds: []string{"tableRow1"}, Content: &model.BlockContentOfLayout{
Layout: &model.BlockContentLayout{
Style: model.BlockContentLayout_TableRows,
},
}}))
s.Add(simple.New(&model.Block{Id: "tableRow1", Content: &model.BlockContentOfTableRow{TableRow: &model.BlockContentTableRow{IsHeader: false}}}))
s.Add(simple.New(&model.Block{Id: "tableColumns", Content: &model.BlockContentOfLayout{
Layout: &model.BlockContentLayout{
Style: model.BlockContentLayout_TableColumns,
},
}}))
msgs, _, err := ApplyState(s, true)
require.NoError(t, err)
assert.Len(t, msgs, 1)
s = s.NewState()
rows := s.Get("tableRow1")
require.NotNil(t, rows)
rows.Model().GetTableRow().IsHeader = true
msgs, _, err = ApplyState(s, true)
require.NoError(t, err)
assert.Len(t, msgs, 1)
})
}

View file

@ -174,7 +174,7 @@ func (s *State) wrapToRow(opId string, parent, b simple.Block) (row simple.Block
if pos == -1 {
return nil, fmt.Errorf("creating row: can't find child[%s] in given parent[%s]", b.Model().Id, parent.Model().Id)
}
s.removeFromCache(parent.Model().ChildrenIds[pos])
// do not need to remove from cache
parent.Model().ChildrenIds[pos] = row.Model().Id
s.addCacheIds(parent.Model(), row.Model().Id)
return
@ -185,6 +185,16 @@ func (s *State) setChildrenIds(parent *model.Block, childrenIds []string) {
s.addCacheIds(parent, childrenIds...)
}
// do not use this method outside of normalization
func (s *State) SetChildrenIds(parent *model.Block, childrenIds []string) {
s.setChildrenIds(parent, childrenIds)
}
// do not use this method outside of normalization
func (s *State) RemoveFromCache(childrenIds []string) {
s.removeFromCache(childrenIds...)
}
func (s *State) removeChildren(parent *model.Block, childrenId string) {
parent.ChildrenIds = slice.RemoveMut(parent.ChildrenIds, childrenId)
s.removeFromCache(childrenId)

View file

@ -118,6 +118,7 @@ type State struct {
localDetails *types.Struct
relationLinks pbtypes.RelationLinks
notifications map[string]*model.Notification
deviceStore map[string]*model.DeviceInfo
migrationVersion uint32
@ -264,6 +265,7 @@ func (s *State) CleanupBlock(id string) bool {
)
for t != nil {
if _, ok = t.blocks[id]; ok {
s.removeFromCache(id)
delete(t.blocks, id)
return true
}
@ -762,6 +764,10 @@ func (s *State) apply(fast, one, withLayouts bool) (msgs []simple.EventMessage,
s.parent.notifications = s.notifications
}
if s.parent != nil && s.deviceStore != nil {
s.parent.deviceStore = s.deviceStore
}
msgs = s.processTrailingDuplicatedEvents(msgs)
log.Debugf("middle: state apply: %d affected; %d for remove; %d copied; %d changes; for a %v", len(affectedIds), len(toRemove), len(s.blocks), len(s.changes), time.Since(st))
return
@ -934,7 +940,7 @@ func (s *State) SetDetails(d *types.Struct) *State {
// SetDetailAndBundledRelation sets the detail value and bundled relation in case it is missing
func (s *State) SetDetailAndBundledRelation(key domain.RelationKey, value *types.Value) {
s.AddBundledRelations(key)
s.AddBundledRelationLinks(key)
s.SetDetail(key.String(), value)
return
}
@ -1403,6 +1409,7 @@ func (s *State) Copy() *State {
originalCreatedTimestamp: s.originalCreatedTimestamp,
fileInfo: s.fileInfo,
notifications: s.notifications,
deviceStore: s.deviceStore,
}
return copy
}
@ -1927,13 +1934,19 @@ func (s *State) SelectRoots(ids []string) []string {
return res
}
func (s *State) AddBundledRelations(keys ...domain.RelationKey) {
links := make([]*model.RelationLink, 0, len(keys))
func (s *State) AddBundledRelationLinks(keys ...domain.RelationKey) {
existingLinks := s.PickRelationLinks()
var links []*model.RelationLink
for _, key := range keys {
rel := bundle.MustGetRelation(key)
links = append(links, &model.RelationLink{Format: rel.Format, Key: rel.Key})
if !existingLinks.Has(key.String()) {
rel := bundle.MustGetRelation(key)
links = append(links, &model.RelationLink{Format: rel.Format, Key: rel.Key})
}
}
if len(links) > 0 {
s.AddRelationLinks(links...)
}
s.AddRelationLinks(links...)
}
func (s *State) GetNotificationById(id string) *model.Notification {
@ -1977,6 +1990,73 @@ func (s *State) findStateWithNonEmptyNotifications() *State {
return iterState
}
func (s *State) ListDevices() map[string]*model.DeviceInfo {
iterState := s.findStateWithDeviceInfo()
if iterState == nil {
return nil
}
return iterState.deviceStore
}
func (s *State) findStateWithDeviceInfo() *State {
iterState := s
for iterState != nil && iterState.deviceStore == nil {
iterState = iterState.parent
}
return iterState
}
func (s *State) AddDevice(device *model.DeviceInfo) {
if s.deviceStore == nil {
s.deviceStore = map[string]*model.DeviceInfo{}
}
if s.parent != nil {
for _, d := range s.parent.ListDevices() {
if _, ok := s.deviceStore[d.Id]; !ok {
s.deviceStore[d.Id] = pbtypes.CopyDevice(d)
}
}
}
if _, ok := s.deviceStore[device.Id]; ok {
return
}
s.deviceStore[device.Id] = device
}
func (s *State) SetDeviceName(id, name string) {
if s.deviceStore == nil {
s.deviceStore = map[string]*model.DeviceInfo{}
}
if s.parent != nil {
for _, d := range s.parent.ListDevices() {
if _, ok := s.deviceStore[d.Id]; !ok {
s.deviceStore[d.Id] = pbtypes.CopyDevice(d)
}
}
}
if _, ok := s.deviceStore[id]; !ok {
device := &model.DeviceInfo{
Id: id,
Name: name,
AddDate: time.Now().Unix(),
}
s.deviceStore[id] = device
return
}
s.deviceStore[id].Name = name
}
func (s *State) GetDevice(id string) *model.DeviceInfo {
iterState := s.findStateWithDeviceInfo()
if iterState == nil {
return nil
}
if device, ok := iterState.deviceStore[id]; ok {
return device
}
return nil
}
// UniqueKeyInternal is the second part of uniquekey.UniqueKey. It used together with smartblock type for the ID derivation
// which will be unique and reproducible within the same space
func (s *State) UniqueKeyInternal() string {

View file

@ -16,6 +16,7 @@ import (
"github.com/anyproto/anytype-heart/core/block/simple/text"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/tests/blockbuilder"
"github.com/anyproto/anytype-heart/util/pbtypes"
@ -2596,3 +2597,226 @@ func TestState_RootId(t *testing.T) {
// assert.True(t, assertAllDetailsLessThenLimit(s.CombinedDetails()))
// })
// }
func TestState_AddDevice(t *testing.T) {
t.Run("add device", func(t *testing.T) {
// given
st := NewDoc("root", nil).(*State)
// when
st.AddDevice(&model.DeviceInfo{
Id: "id",
Name: "test",
})
// then
assert.NotNil(t, st.deviceStore["id"])
})
t.Run("add device - device exist", func(t *testing.T) {
// given
st := NewDoc("root", nil).(*State)
st.AddDevice(&model.DeviceInfo{
Id: "id",
Name: "test",
})
newState := st.NewState()
// when
newState.AddDevice(&model.DeviceInfo{
Id: "id",
Name: "test1",
})
// then
assert.NotNil(t, st.deviceStore["id"])
assert.Equal(t, "test", st.deviceStore["id"].Name)
})
}
func TestState_GetDevice(t *testing.T) {
t.Run("get device, device not exist", func(t *testing.T) {
// given
st := NewDoc("root", nil).(*State)
// when
device := st.GetDevice("id")
// then
assert.Nil(t, device)
})
t.Run("add device - device exist", func(t *testing.T) {
// given
st := NewDoc("root", nil).(*State)
st.AddDevice(&model.DeviceInfo{
Id: "id",
Name: "test",
})
// when
device := st.GetDevice("id")
// then
assert.NotNil(t, device)
})
t.Run("add device - device with given id not exist", func(t *testing.T) {
// given
st := NewDoc("root", nil).(*State)
st.AddDevice(&model.DeviceInfo{
Id: "id",
Name: "test",
})
// when
device := st.GetDevice("id1")
// then
assert.Nil(t, device)
})
}
func TestState_ListDevices(t *testing.T) {
t.Run("list devices, no devices", func(t *testing.T) {
// given
st := NewDoc("root", nil).(*State)
// when
devices := st.ListDevices()
// then
assert.Empty(t, devices)
})
t.Run("list devices", func(t *testing.T) {
// given
st := NewDoc("root", nil).(*State)
st.AddDevice(&model.DeviceInfo{
Id: "id",
Name: "test",
})
// when
devices := st.ListDevices()
// then
assert.Len(t, devices, 1)
})
}
func TestState_SetDeviceName(t *testing.T) {
t.Run("set device name, device not exist", func(t *testing.T) {
// given
st := NewDoc("root", nil).(*State)
// when
st.SetDeviceName("id", "test")
// then
assert.NotNil(t, st.deviceStore["id"])
assert.Equal(t, st.deviceStore["id"].Name, "test")
})
t.Run("set device name, device exists", func(t *testing.T) {
// given
st := NewDoc("root", nil).(*State)
st.AddDevice(&model.DeviceInfo{
Id: "id",
Name: "test",
})
newState := st.NewState()
// when
newState.SetDeviceName("id", "test1")
// then
assert.NotNil(t, newState.deviceStore["id"])
assert.Equal(t, newState.deviceStore["id"].Name, "test1")
})
}
func TestAddBundledRealtionLinks(t *testing.T) {
t.Run("with relationLinks in state", func(t *testing.T) {
t.Run("empty", func(t *testing.T) {
st := &State{
relationLinks: []*model.RelationLink{},
}
st.AddBundledRelationLinks(bundle.RelationKeyName, bundle.RelationKeyPriority)
want := &State{
relationLinks: []*model.RelationLink{
{
Key: bundle.RelationKeyName.String(),
Format: model.RelationFormat_shorttext,
},
{
Key: bundle.RelationKeyPriority.String(),
Format: model.RelationFormat_number,
},
},
}
assert.Equal(t, want, st)
})
t.Run("one already exists, one not", func(t *testing.T) {
st := &State{
relationLinks: []*model.RelationLink{
{
Key: bundle.RelationKeyName.String(),
Format: model.RelationFormat_shorttext,
},
},
}
st.AddBundledRelationLinks(bundle.RelationKeyName, bundle.RelationKeyPriority)
want := &State{
relationLinks: []*model.RelationLink{
{
Key: bundle.RelationKeyName.String(),
Format: model.RelationFormat_shorttext,
},
{
Key: bundle.RelationKeyPriority.String(),
Format: model.RelationFormat_number,
},
},
}
assert.Equal(t, want, st)
})
})
t.Run("with relationLinks only in parent state", func(t *testing.T) {
st := &State{
relationLinks: nil,
parent: &State{
relationLinks: []*model.RelationLink{
{
Key: bundle.RelationKeyName.String(),
Format: model.RelationFormat_shorttext,
},
},
},
}
st.AddBundledRelationLinks(bundle.RelationKeyName, bundle.RelationKeyPriority)
want := &State{
relationLinks: []*model.RelationLink{
{
Key: bundle.RelationKeyName.String(),
Format: model.RelationFormat_shorttext,
},
{
Key: bundle.RelationKeyPriority.String(),
Format: model.RelationFormat_number,
},
},
parent: &State{
relationLinks: []*model.RelationLink{
{
Key: bundle.RelationKeyName.String(),
Format: model.RelationFormat_shorttext,
},
},
},
}
assert.Equal(t, want, st)
})
}

View file

@ -1,6 +1,7 @@
package table
import (
"errors"
"fmt"
"sort"
@ -28,8 +29,8 @@ func NewBlock(b *model.Block) simple.Block {
type Block interface {
simple.Block
Normalize(s *state.State) error
Duplicate(s *state.State) (newID string, visitedIds []string, blocks []simple.Block, err error)
Normalize(s *state.State) error
}
type block struct {
@ -40,37 +41,6 @@ func (b *block) Copy() simple.Block {
return NewBlock(pbtypes.CopyBlock(b.Model()))
}
func (b *block) Normalize(s *state.State) error {
tb, err := NewTable(s, b.Id)
if err != nil {
log.Errorf("normalize table %s: broken table state: %s", b.Model().Id, err)
return nil
}
colIdx := map[string]int{}
for i, c := range tb.ColumnIDs() {
colIdx[c] = i
}
for _, rowID := range tb.RowIDs() {
row := s.Get(rowID)
// Fix data integrity by adding missing row
if row == nil {
row = makeRow(rowID)
if !s.Add(row) {
return fmt.Errorf("add missing row block %s", rowID)
}
continue
}
normalizeRow(colIdx, row)
}
if err := normalizeRows(s, tb); err != nil {
return fmt.Errorf("normalize rows: %w", err)
}
return nil
}
func (b *block) Duplicate(s *state.State) (newID string, visitedIds []string, blocks []simple.Block, err error) {
tb, err := NewTable(s, b.Id)
if err != nil {
@ -144,6 +114,25 @@ func (b *block) Duplicate(s *state.State) (newID string, visitedIds []string, bl
return block.Model().Id, visitedIds, blocks, nil
}
func (b *block) Normalize(s *state.State) error {
tb, err := NewTable(s, b.Id)
if err != nil {
log.Errorf("normalize table %s: broken table state: %s", b.Id, err)
if !s.Unlink(b.Id) {
log.Errorf("failed to unlink table block: %s", b.Id)
}
return nil
}
tb.normalizeColumns()
tb.normalizeRows()
if err = tb.normalizeHeaderRows(); err != nil {
// actually we cannot get error here, as all rows are checked in normalizeRows
log.Errorf("normalize header rows: %v", err)
}
return nil
}
type rowSort struct {
indices []int
cells []string
@ -164,14 +153,13 @@ func (r *rowSort) Swap(i, j int) {
r.cells[i], r.cells[j] = r.cells[j], r.cells[i]
}
func normalizeRows(s *state.State, tb *Table) error {
rows := s.Get(tb.Rows().Id)
func (tb Table) normalizeHeaderRows() error {
rows := tb.s.Get(tb.Rows().Id)
var headers []string
regular := make([]string, 0, len(rows.Model().ChildrenIds))
for _, rowID := range rows.Model().ChildrenIds {
row, err := pickRow(s, rowID)
row, err := pickRow(tb.s, rowID)
if err != nil {
return fmt.Errorf("pick row %s: %w", rowID, err)
}
@ -183,30 +171,37 @@ func normalizeRows(s *state.State, tb *Table) error {
}
}
// nolint:gocritic
rows.Model().ChildrenIds = append(headers, regular...)
tb.s.SetChildrenIds(rows.Model(), append(headers, regular...))
return nil
}
func normalizeRow(colIdx map[string]int, row simple.Block) {
func (tb Table) normalizeRow(colIdx map[string]int, row simple.Block) {
if row == nil || row.Model() == nil {
return
}
if colIdx == nil {
colIdx = tb.MakeColumnIndex()
}
rs := &rowSort{
cells: make([]string, 0, len(row.Model().ChildrenIds)),
indices: make([]int, 0, len(row.Model().ChildrenIds)),
}
toRemove := []string{}
for _, id := range row.Model().ChildrenIds {
_, colID, err := ParseCellID(id)
if err != nil {
log.Warnf("normalize row %s: discard cell %s: invalid id", row.Model().Id, id)
log.Warnf("normalize row %s: move cell %s under the table: invalid id", row.Model().Id, id)
toRemove = append(toRemove, id)
rs.touched = true
continue
}
v, ok := colIdx[colID]
if !ok {
log.Warnf("normalize row %s: discard cell %s: column %s not found", row.Model().Id, id, colID)
log.Warnf("normalize row %s: move cell %s under the table: column %s not found", row.Model().Id, id, colID)
toRemove = append(toRemove, id)
rs.touched = true
continue
}
@ -216,6 +211,88 @@ func normalizeRow(colIdx map[string]int, row simple.Block) {
sort.Sort(rs)
if rs.touched {
row.Model().ChildrenIds = rs.cells
tb.MoveBlocksUnderTheTable(toRemove...)
tb.s.SetChildrenIds(row.Model(), rs.cells)
}
}
func (tb Table) normalizeColumns() {
var (
invalidFound bool
colIds = make([]string, 0)
toRemove = make([]string, 0)
)
for _, colId := range tb.ColumnIDs() {
if _, err := pickColumn(tb.s, colId); err != nil {
invalidFound = true
switch {
case errors.Is(err, errColumnNotFound):
// Fix data integrity by adding missing column
log.Warnf("normalize columns '%s': column '%s' is not found: recreating it", tb.Columns().Id, colId)
col := makeColumn(colId)
if !tb.s.Add(col) {
log.Errorf("add missing column block %s", colId)
toRemove = append(toRemove, colId)
continue
}
colIds = append(colIds, colId)
case errors.Is(err, errNotAColumn):
log.Warnf("normalize columns '%s': block '%s' is not a column: move it under the table", tb.Columns().Id, colId)
tb.MoveBlocksUnderTheTable(colId)
default:
log.Errorf("pick column %s: %v", colId, err)
toRemove = append(toRemove, colId)
}
continue
}
colIds = append(colIds, colId)
}
if invalidFound {
tb.s.RemoveFromCache(toRemove)
tb.s.SetChildrenIds(tb.Columns(), colIds)
}
}
func (tb Table) normalizeRows() {
var (
invalidFound bool
rowIds = make([]string, 0)
toRemove = make([]string, 0)
colIdx = tb.MakeColumnIndex()
)
for _, rowId := range tb.RowIDs() {
row, err := getRow(tb.s, rowId)
if err != nil {
invalidFound = true
switch {
case errors.Is(err, errRowNotFound):
// Fix data integrity by adding missing row
log.Warnf("normalize rows '%s': row '%s' is not found: recreating it", tb.Rows().Id, rowId)
row = makeRow(rowId)
if !tb.s.Add(row) {
log.Errorf("add missing row block %s", rowId)
toRemove = append(toRemove, rowId)
continue
}
rowIds = append(rowIds, rowId)
case errors.Is(err, errNotARow):
log.Warnf("normalize rows '%s': block '%s' is not a row: move it under the table", tb.Rows().Id, rowId)
tb.MoveBlocksUnderTheTable(rowId)
default:
log.Errorf("get row %s: %v", rowId, err)
toRemove = append(toRemove, rowId)
}
continue
}
tb.normalizeRow(colIdx, row)
rowIds = append(rowIds, rowId)
}
if invalidFound {
tb.s.RemoveFromCache(toRemove)
tb.s.SetChildrenIds(tb.Rows(), rowIds)
}
}

View file

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/core/block/simple"
"github.com/anyproto/anytype-heart/core/block/simple/base"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
@ -18,34 +19,41 @@ func TestNormalize(t *testing.T) {
want *state.State
}{
{
name: "empty",
name: "empty table should remain empty",
source: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{}),
want: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{}),
},
{
name: "invalid ids",
name: "cells with invalid ids are moved under the table",
source: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{
{"row1-c11", "row1-col2"},
{"row2-col3"},
{"row2-col3", "cell"},
}),
want: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{
{"row1-col2"},
{},
}),
{"row1-c11", "row1-col2"},
{"row2-col3", "cell"},
}, withChangedChildren(map[string][]string{
"root": {"table", "row2-col3", "cell", "row1-c11"},
"row1": {"row1-col2"},
"row2": {},
})),
},
{
name: "wrong column order",
name: "wrong cells order -> do sorting and move invalid cells under the table",
source: mkTestTable([]string{"col1", "col2", "col3"}, []string{"row1", "row2"}, [][]string{
{"row1-col3", "row1-col1", "row1-col2"},
{"row2-col3", "row2-c1", "row2-col1"},
}),
want: mkTestTable([]string{"col1", "col2", "col3"}, []string{"row1", "row2"}, [][]string{
{"row1-col1", "row1-col2", "row1-col3"},
{"row2-col1", "row2-col3"},
}),
{"row2-col3", "row2-c1", "row2-col1"},
}, withChangedChildren(map[string][]string{
"root": {"table", "row2-c1"},
"row2": {"row2-col1", "row2-col3"},
})),
},
{
name: "wrong place for header rows",
name: "wrong place for header rows -> do sorting",
source: mkTestTable([]string{"col1", "col2", "col3"}, []string{"row1", "row2", "row3"}, nil,
withRowBlockContents(map[string]*model.BlockContentTableRow{
"row3": {IsHeader: true},
@ -55,45 +63,74 @@ func TestNormalize(t *testing.T) {
"row3": {IsHeader: true},
})),
},
{
name: "cell is a child of rows, not row -> move under the table",
source: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{
{"row1-col1", "row1-col2"}, {"row2-col1", "row2-col2"},
}, withChangedChildren(map[string][]string{
"rows": {"row1", "row1-col2", "row2"},
"row1": {"row1-col1"},
})),
want: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{
{"row1-col1", "row1-col2"}, {"row2-col1", "row2-col2"},
}, withChangedChildren(map[string][]string{
"root": {"table", "row1-col2"},
"row1": {"row1-col1"},
})),
},
{
name: "columns contain invalid children -> move under the table",
source: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{
{"row1-col1", "row1-col2"}, {"row2-col1", "row2-col2"},
}, withChangedChildren(map[string][]string{
"columns": {"col1", "col2", "row1-col2"},
"row1": {"row1-col1"},
})),
want: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{
{"row1-col1", "row1-col2"}, {"row2-col1", "row2-col2"},
}, withChangedChildren(map[string][]string{
"root": {"table", "row1-col2"},
"row1": {"row1-col1"},
})),
},
{
name: "table block contains invalid children -> table is dropped",
source: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{}, withChangedChildren(map[string][]string{
"table": {"columns"},
})),
want: state.NewDoc("root", map[string]simple.Block{"root": simple.New(&model.Block{Id: "root"})}).NewState(),
},
{
name: "missed column is recreated",
source: mkTestTable([]string{"col1"}, []string{"row1", "row2"}, [][]string{}, withChangedChildren(map[string][]string{
"columns": {"col1", "col2"},
})),
want: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{}),
},
{
name: "missed row is recreated",
source: mkTestTable([]string{"col1", "col2"}, []string{"row1"}, [][]string{}, withChangedChildren(map[string][]string{
"rows": {"row1", "row2"},
})),
want: mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2"}, [][]string{}),
},
} {
t.Run(tc.name, func(t *testing.T) {
tb, err := NewTable(tc.source, "table")
require.NoError(t, err)
// given
st := tc.source.Copy()
err = tb.block.(Block).Normalize(st)
require.NoError(t, err)
tb := st.Pick("table")
require.NotNil(t, tb)
// when
err := tb.(Block).Normalize(st)
// then
require.NoError(t, err)
assert.Equal(t, tc.want.Blocks(), st.Blocks())
})
}
}
func TestNormalizeAbsentRow(t *testing.T) {
source := mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2", "row3"}, [][]string{
{"row1-c11", "row1-col2"},
{"row2-col3"},
})
source.CleanupBlock("row3")
want := mkTestTable([]string{"col1", "col2"}, []string{"row1", "row2", "row3"}, [][]string{
{"row1-col2"},
{},
{},
})
tb, err := NewTable(source, "table")
require.NoError(t, err)
st := source.Copy()
err = tb.block.(Block).Normalize(st)
require.NoError(t, err)
assert.Equal(t, want.Blocks(), st.Blocks())
}
func TestDuplicate(t *testing.T) {
s := mkTestTable([]string{"col1", "col2", "col3"}, []string{"row1", "row2"},
[][]string{

View file

@ -0,0 +1,782 @@
package table
import (
"errors"
"fmt"
"sort"
"github.com/globalsign/mgo/bson"
"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/simple"
"github.com/anyproto/anytype-heart/core/block/simple/text"
"github.com/anyproto/anytype-heart/core/block/source"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
// nolint:revive,interfacebloat
type TableEditor interface {
TableCreate(s *state.State, req pb.RpcBlockTableCreateRequest) (string, error)
CellCreate(s *state.State, rowID string, colID string, b *model.Block) (string, error)
RowCreate(s *state.State, req pb.RpcBlockTableRowCreateRequest) (string, error)
RowDelete(s *state.State, req pb.RpcBlockTableRowDeleteRequest) error
RowDuplicate(s *state.State, req pb.RpcBlockTableRowDuplicateRequest) (newRowID string, err error)
// RowMove is done via BlockListMoveToExistingObject
RowListFill(s *state.State, req pb.RpcBlockTableRowListFillRequest) error
RowListClean(s *state.State, req pb.RpcBlockTableRowListCleanRequest) error
RowSetHeader(s *state.State, req pb.RpcBlockTableRowSetHeaderRequest) error
ColumnCreate(s *state.State, req pb.RpcBlockTableColumnCreateRequest) (string, error)
ColumnDelete(s *state.State, req pb.RpcBlockTableColumnDeleteRequest) error
ColumnDuplicate(s *state.State, req pb.RpcBlockTableColumnDuplicateRequest) (id string, err error)
ColumnMove(s *state.State, req pb.RpcBlockTableColumnMoveRequest) error
ColumnListFill(s *state.State, req pb.RpcBlockTableColumnListFillRequest) error
Expand(s *state.State, req pb.RpcBlockTableExpandRequest) error
Sort(s *state.State, req pb.RpcBlockTableSortRequest) error
cleanupTables(_ smartblock.ApplyInfo) error
cloneColumnStyles(s *state.State, srcColID string, targetColID string) error
}
type editor struct {
sb smartblock.SmartBlock
generateRowID func() string
generateColID func() string
}
var _ TableEditor = &editor{}
func NewEditor(sb smartblock.SmartBlock) TableEditor {
genID := func() string {
return bson.NewObjectId().Hex()
}
t := editor{
sb: sb,
generateRowID: genID,
generateColID: genID,
}
if sb != nil {
sb.AddHook(t.cleanupTables, smartblock.HookOnBlockClose)
}
return &t
}
func (t *editor) TableCreate(s *state.State, req pb.RpcBlockTableCreateRequest) (string, error) {
if t.sb != nil {
if err := t.sb.Restrictions().Object.Check(model.Restrictions_Blocks); err != nil {
return "", err
}
}
tableBlock := simple.New(&model.Block{
Content: &model.BlockContentOfTable{
Table: &model.BlockContentTable{},
},
})
if !s.Add(tableBlock) {
return "", fmt.Errorf("add table block")
}
if err := s.InsertTo(req.TargetId, req.Position, tableBlock.Model().Id); err != nil {
return "", fmt.Errorf("insert block: %w", err)
}
columnIds := make([]string, 0, req.Columns)
for i := uint32(0); i < req.Columns; i++ {
id, err := t.addColumnHeader(s)
if err != nil {
return "", err
}
columnIds = append(columnIds, id)
}
columnsLayout := simple.New(&model.Block{
ChildrenIds: columnIds,
Content: &model.BlockContentOfLayout{
Layout: &model.BlockContentLayout{
Style: model.BlockContentLayout_TableColumns,
},
},
})
if !s.Add(columnsLayout) {
return "", fmt.Errorf("add columns block")
}
rowIDs := make([]string, 0, req.Rows)
for i := uint32(0); i < req.Rows; i++ {
id, err := t.addRow(s)
if err != nil {
return "", err
}
rowIDs = append(rowIDs, id)
}
rowsLayout := simple.New(&model.Block{
ChildrenIds: rowIDs,
Content: &model.BlockContentOfLayout{
Layout: &model.BlockContentLayout{
Style: model.BlockContentLayout_TableRows,
},
},
})
if !s.Add(rowsLayout) {
return "", fmt.Errorf("add rows block")
}
tableBlock.Model().ChildrenIds = []string{columnsLayout.Model().Id, rowsLayout.Model().Id}
if !req.WithHeaderRow {
return tableBlock.Model().Id, nil
}
if len(rowIDs) == 0 {
return "", fmt.Errorf("no rows to make header row")
}
headerID := rowIDs[0]
if err := t.RowSetHeader(s, pb.RpcBlockTableRowSetHeaderRequest{
TargetId: headerID,
IsHeader: true,
}); err != nil {
return "", fmt.Errorf("row set header: %w", err)
}
if err := t.RowListFill(s, pb.RpcBlockTableRowListFillRequest{
BlockIds: []string{headerID},
}); err != nil {
return "", fmt.Errorf("fill header row: %w", err)
}
row, err := getRow(s, headerID)
if err != nil {
return "", fmt.Errorf("get header row: %w", err)
}
for _, cellID := range row.Model().ChildrenIds {
cell := s.Get(cellID)
if cell == nil {
return "", fmt.Errorf("get header cell id %s", cellID)
}
cell.Model().BackgroundColor = "grey"
}
return tableBlock.Model().Id, nil
}
func (t *editor) CellCreate(s *state.State, rowID string, colID string, b *model.Block) (string, error) {
tb, err := NewTable(s, rowID)
if err != nil {
return "", fmt.Errorf("initialize table state: %w", err)
}
row, err := getRow(s, rowID)
if err != nil {
return "", fmt.Errorf("get row: %w", err)
}
if _, err = pickColumn(s, colID); err != nil {
return "", fmt.Errorf("pick column: %w", err)
}
cellID, err := addCell(s, rowID, colID)
if err != nil {
return "", fmt.Errorf("add cell: %w", err)
}
cell := s.Get(cellID)
cell.Model().Content = b.Content
if err := s.InsertTo(rowID, model.Block_Inner, cellID); err != nil {
return "", fmt.Errorf("insert to: %w", err)
}
tb.normalizeRow(nil, row)
return cellID, nil
}
func (t *editor) RowCreate(s *state.State, req pb.RpcBlockTableRowCreateRequest) (string, error) {
switch req.Position {
case model.Block_Top, model.Block_Bottom:
case model.Block_Inner:
tb, err := NewTable(s, req.TargetId)
if err != nil {
return "", fmt.Errorf("initialize table state: %w", err)
}
req.TargetId = tb.Rows().Id
default:
return "", fmt.Errorf("position is not supported")
}
rowID, err := t.addRow(s)
if err != nil {
return "", err
}
if err := s.InsertTo(req.TargetId, req.Position, rowID); err != nil {
return "", fmt.Errorf("insert row: %w", err)
}
return rowID, nil
}
func (t *editor) RowDelete(s *state.State, req pb.RpcBlockTableRowDeleteRequest) error {
_, err := pickRow(s, req.TargetId)
if err != nil {
return fmt.Errorf("pick target row: %w", err)
}
if !s.Unlink(req.TargetId) {
return fmt.Errorf("unlink row block")
}
return nil
}
func (t *editor) RowDuplicate(s *state.State, req pb.RpcBlockTableRowDuplicateRequest) (newRowID string, err error) {
if req.Position != model.Block_Top && req.Position != model.Block_Bottom {
return "", fmt.Errorf("position %s is not supported", model.BlockPosition_name[int32(req.Position)])
}
srcRow, err := pickRow(s, req.BlockId)
if err != nil {
return "", fmt.Errorf("pick source row: %w", err)
}
if _, err = pickRow(s, req.TargetId); err != nil {
return "", fmt.Errorf("pick target row: %w", err)
}
newRow := srcRow.Copy()
newRow.Model().Id = t.generateRowID()
if !s.Add(newRow) {
return "", fmt.Errorf("add new row %s", newRow.Model().Id)
}
if err = s.InsertTo(req.TargetId, req.Position, newRow.Model().Id); err != nil {
return "", fmt.Errorf("insert column: %w", err)
}
for i, srcID := range newRow.Model().ChildrenIds {
cell := s.Pick(srcID)
if cell == nil {
return "", fmt.Errorf("cell %s is not found", srcID)
}
_, colID, err := ParseCellID(srcID)
if err != nil {
return "", fmt.Errorf("parse cell id %s: %w", srcID, err)
}
newCell := cell.Copy()
newCell.Model().Id = MakeCellID(newRow.Model().Id, colID)
if !s.Add(newCell) {
return "", fmt.Errorf("add new cell %s", newCell.Model().Id)
}
newRow.Model().ChildrenIds[i] = newCell.Model().Id
}
return newRow.Model().Id, nil
}
func (t *editor) RowListFill(s *state.State, req pb.RpcBlockTableRowListFillRequest) error {
if len(req.BlockIds) == 0 {
return fmt.Errorf("empty row list")
}
tb, err := NewTable(s, req.BlockIds[0])
if err != nil {
return fmt.Errorf("init table: %w", err)
}
columns := tb.ColumnIDs()
for _, rowID := range req.BlockIds {
row, err := getRow(s, rowID)
if err != nil {
return fmt.Errorf("get row %s: %w", rowID, err)
}
newIds := make([]string, 0, len(columns))
for _, colID := range columns {
id := MakeCellID(rowID, colID)
newIds = append(newIds, id)
if !s.Exists(id) {
_, err := addCell(s, rowID, colID)
if err != nil {
return fmt.Errorf("add cell %s: %w", id, err)
}
}
}
row.Model().ChildrenIds = newIds
}
return nil
}
func (t *editor) RowListClean(s *state.State, req pb.RpcBlockTableRowListCleanRequest) error {
if len(req.BlockIds) == 0 {
return fmt.Errorf("empty row list")
}
for _, rowID := range req.BlockIds {
row, err := pickRow(s, rowID)
if err != nil {
return fmt.Errorf("pick row: %w", err)
}
for _, cellID := range row.Model().ChildrenIds {
cell := s.Pick(cellID)
if v, ok := cell.(text.Block); ok && v.IsEmpty() {
s.Unlink(cellID)
}
}
}
return nil
}
func (t *editor) RowSetHeader(s *state.State, req pb.RpcBlockTableRowSetHeaderRequest) error {
tb, err := NewTable(s, req.TargetId)
if err != nil {
return fmt.Errorf("init table: %w", err)
}
row, err := getRow(s, req.TargetId)
if err != nil {
return fmt.Errorf("get target row: %w", err)
}
if row.Model().GetTableRow().IsHeader != req.IsHeader {
row.Model().GetTableRow().IsHeader = req.IsHeader
err = tb.normalizeHeaderRows()
if err != nil {
return fmt.Errorf("normalize rows: %w", err)
}
}
return nil
}
func (t *editor) ColumnCreate(s *state.State, req pb.RpcBlockTableColumnCreateRequest) (string, error) {
switch req.Position {
case model.Block_Left:
req.Position = model.Block_Top
if _, err := pickColumn(s, req.TargetId); err != nil {
return "", fmt.Errorf("pick column: %w", err)
}
case model.Block_Right:
req.Position = model.Block_Bottom
if _, err := pickColumn(s, req.TargetId); err != nil {
return "", fmt.Errorf("pick column: %w", err)
}
case model.Block_Inner:
tb, err := NewTable(s, req.TargetId)
if err != nil {
return "", fmt.Errorf("initialize table state: %w", err)
}
req.TargetId = tb.Columns().Id
default:
return "", fmt.Errorf("position is not supported")
}
colID, err := t.addColumnHeader(s)
if err != nil {
return "", err
}
if err = s.InsertTo(req.TargetId, req.Position, colID); err != nil {
return "", fmt.Errorf("insert column header: %w", err)
}
return colID, t.cloneColumnStyles(s, req.TargetId, colID)
}
func (t *editor) ColumnDelete(s *state.State, req pb.RpcBlockTableColumnDeleteRequest) error {
_, err := pickColumn(s, req.TargetId)
if err != nil {
return fmt.Errorf("pick target column: %w", err)
}
tb, err := NewTable(s, req.TargetId)
if err != nil {
return fmt.Errorf("initialize table state: %w", err)
}
for _, rowID := range tb.RowIDs() {
row, err := pickRow(s, rowID)
if err != nil {
return fmt.Errorf("pick row %s: %w", rowID, err)
}
for _, cellID := range row.Model().ChildrenIds {
_, colID, err := ParseCellID(cellID)
if err != nil {
return fmt.Errorf("parse cell id %s: %w", cellID, err)
}
if colID == req.TargetId {
if !s.Unlink(cellID) {
return fmt.Errorf("unlink cell %s", cellID)
}
break
}
}
}
if !s.Unlink(req.TargetId) {
return fmt.Errorf("unlink column header")
}
return nil
}
func (t *editor) ColumnDuplicate(s *state.State, req pb.RpcBlockTableColumnDuplicateRequest) (id string, err error) {
switch req.Position {
case model.Block_Left:
req.Position = model.Block_Top
case model.Block_Right:
req.Position = model.Block_Bottom
default:
return "", fmt.Errorf("position is not supported")
}
srcCol, err := pickColumn(s, req.BlockId)
if err != nil {
return "", fmt.Errorf("pick source column: %w", err)
}
_, err = pickColumn(s, req.TargetId)
if err != nil {
return "", fmt.Errorf("pick target column: %w", err)
}
tb, err := NewTable(s, req.TargetId)
if err != nil {
return "", fmt.Errorf("init table block: %w", err)
}
newCol := srcCol.Copy()
newCol.Model().Id = t.generateColID()
if !s.Add(newCol) {
return "", fmt.Errorf("add column block")
}
if err = s.InsertTo(req.TargetId, req.Position, newCol.Model().Id); err != nil {
return "", fmt.Errorf("insert column: %w", err)
}
colIdx := tb.MakeColumnIndex()
for _, rowID := range tb.RowIDs() {
row, err := getRow(s, rowID)
if err != nil {
return "", fmt.Errorf("get row %s: %w", rowID, err)
}
var cellID string
for _, id := range row.Model().ChildrenIds {
_, colID, err := ParseCellID(id)
if err != nil {
return "", fmt.Errorf("parse cell %s in row %s: %w", cellID, rowID, err)
}
if colID == req.BlockId {
cellID = id
break
}
}
if cellID == "" {
continue
}
cell := s.Pick(cellID)
if cell == nil {
return "", fmt.Errorf("cell %s is not found", cellID)
}
cell = cell.Copy()
cell.Model().Id = MakeCellID(rowID, newCol.Model().Id)
if !s.Add(cell) {
return "", fmt.Errorf("add cell block")
}
row.Model().ChildrenIds = append(row.Model().ChildrenIds, cell.Model().Id)
tb.normalizeRow(colIdx, row)
}
return newCol.Model().Id, nil
}
func (t *editor) ColumnMove(s *state.State, req pb.RpcBlockTableColumnMoveRequest) error {
switch req.Position {
case model.Block_Left:
req.Position = model.Block_Top
case model.Block_Right:
req.Position = model.Block_Bottom
default:
return fmt.Errorf("position is not supported")
}
_, err := pickColumn(s, req.TargetId)
if err != nil {
return fmt.Errorf("get target column: %w", err)
}
_, err = pickColumn(s, req.DropTargetId)
if err != nil {
return fmt.Errorf("get drop target column: %w", err)
}
tb, err := NewTable(s, req.TargetId)
if err != nil {
return fmt.Errorf("init table block: %w", err)
}
if !s.Unlink(req.TargetId) {
return fmt.Errorf("unlink target column")
}
if err = s.InsertTo(req.DropTargetId, req.Position, req.TargetId); err != nil {
return fmt.Errorf("insert column: %w", err)
}
colIdx := tb.MakeColumnIndex()
for _, id := range tb.RowIDs() {
row, err := getRow(s, id)
if err != nil {
return fmt.Errorf("get row %s: %w", id, err)
}
tb.normalizeRow(colIdx, row)
}
return nil
}
func (t *editor) ColumnListFill(s *state.State, req pb.RpcBlockTableColumnListFillRequest) error {
if len(req.BlockIds) == 0 {
return fmt.Errorf("empty row list")
}
tb, err := NewTable(s, req.BlockIds[0])
if err != nil {
return fmt.Errorf("init table: %w", err)
}
rows := tb.RowIDs()
for _, colID := range req.BlockIds {
for _, rowID := range rows {
id := MakeCellID(rowID, colID)
if s.Exists(id) {
continue
}
_, err := addCell(s, rowID, colID)
if err != nil {
return fmt.Errorf("add cell %s: %w", id, err)
}
row, err := getRow(s, rowID)
if err != nil {
return fmt.Errorf("get row %s: %w", rowID, err)
}
row.Model().ChildrenIds = append(row.Model().ChildrenIds, id)
}
}
colIdx := tb.MakeColumnIndex()
for _, rowID := range rows {
row, err := getRow(s, rowID)
if err != nil {
return fmt.Errorf("get row %s: %w", rowID, err)
}
tb.normalizeRow(colIdx, row)
}
return nil
}
func (t *editor) Expand(s *state.State, req pb.RpcBlockTableExpandRequest) error {
tb, err := NewTable(s, req.TargetId)
if err != nil {
return fmt.Errorf("init table block: %w", err)
}
for i := uint32(0); i < req.Columns; i++ {
_, err := t.ColumnCreate(s, pb.RpcBlockTableColumnCreateRequest{
TargetId: req.TargetId,
Position: model.Block_Inner,
})
if err != nil {
return fmt.Errorf("create column: %w", err)
}
}
for i := uint32(0); i < req.Rows; i++ {
rows := tb.Rows()
_, err := t.RowCreate(s, pb.RpcBlockTableRowCreateRequest{
TargetId: rows.ChildrenIds[len(rows.ChildrenIds)-1],
Position: model.Block_Bottom,
})
if err != nil {
return fmt.Errorf("create row: %w", err)
}
}
return nil
}
func (t *editor) Sort(s *state.State, req pb.RpcBlockTableSortRequest) error {
_, err := pickColumn(s, req.ColumnId)
if err != nil {
return fmt.Errorf("pick column: %w", err)
}
tb, err := NewTable(s, req.ColumnId)
if err != nil {
return fmt.Errorf("init table block: %w", err)
}
rows := s.Get(tb.Rows().Id)
sorter := tableSorter{
rowIDs: make([]string, 0, len(rows.Model().ChildrenIds)),
values: make([]string, len(rows.Model().ChildrenIds)),
}
var headers []string
var i int
for _, rowID := range rows.Model().ChildrenIds {
row, err := pickRow(s, rowID)
if err != nil {
return fmt.Errorf("pick row %s: %w", rowID, err)
}
if row.Model().GetTableRow().GetIsHeader() {
headers = append(headers, rowID)
continue
}
sorter.rowIDs = append(sorter.rowIDs, rowID)
for _, cellID := range row.Model().ChildrenIds {
_, colID, err := ParseCellID(cellID)
if err != nil {
return fmt.Errorf("parse cell id %s: %w", cellID, err)
}
if colID == req.ColumnId {
cell := s.Pick(cellID)
if cell == nil {
return fmt.Errorf("cell %s is not found", cellID)
}
sorter.values[i] = cell.Model().GetText().GetText()
}
}
i++
}
if req.Type == model.BlockContentDataviewSort_Asc {
sort.Stable(sorter)
} else {
sort.Stable(sort.Reverse(sorter))
}
// nolint:gocritic
rows.Model().ChildrenIds = append(headers, sorter.rowIDs...)
return nil
}
func (t *editor) cleanupTables(_ smartblock.ApplyInfo) error {
if t.sb == nil {
return fmt.Errorf("nil smartblock")
}
s := t.sb.NewState()
err := s.Iterate(func(b simple.Block) bool {
if b.Model().GetTable() == nil {
return true
}
tb, err := NewTable(s, b.Model().Id)
if err != nil {
log.Errorf("cleanup: init table %s: %s", b.Model().Id, err)
return true
}
err = t.RowListClean(s, pb.RpcBlockTableRowListCleanRequest{
BlockIds: tb.RowIDs(),
})
if err != nil {
log.Errorf("cleanup table %s: %s", b.Model().Id, err)
return true
}
return true
})
if err != nil {
log.Errorf("cleanup iterate: %s", err)
}
if err = t.sb.Apply(s, smartblock.KeepInternalFlags); err != nil {
if errors.Is(err, source.ErrReadOnly) {
return nil
}
log.Errorf("cleanup apply: %s", err)
}
return nil
}
func (t *editor) cloneColumnStyles(s *state.State, srcColID, targetColID string) error {
tb, err := NewTable(s, srcColID)
if err != nil {
return fmt.Errorf("init table block: %w", err)
}
colIdx := tb.MakeColumnIndex()
for _, rowID := range tb.RowIDs() {
row, err := pickRow(s, rowID)
if err != nil {
return fmt.Errorf("pick row: %w", err)
}
var protoBlock simple.Block
for _, cellID := range row.Model().ChildrenIds {
_, colID, err := ParseCellID(cellID)
if err != nil {
return fmt.Errorf("parse cell id: %w", err)
}
if colID == srcColID {
protoBlock = s.Pick(cellID)
}
}
if protoBlock != nil && protoBlock.Model().BackgroundColor != "" {
targetCellID := MakeCellID(rowID, targetColID)
if !s.Exists(targetCellID) {
_, err := addCell(s, rowID, targetColID)
if err != nil {
return fmt.Errorf("add cell: %w", err)
}
}
cell := s.Get(targetCellID)
cell.Model().BackgroundColor = protoBlock.Model().BackgroundColor
row = s.Get(row.Model().Id)
row.Model().ChildrenIds = append(row.Model().ChildrenIds, targetCellID)
tb.normalizeRow(colIdx, row)
}
}
return nil
}
func (t *editor) addColumnHeader(s *state.State) (string, error) {
b := simple.New(&model.Block{
Id: t.generateColID(),
Content: &model.BlockContentOfTableColumn{
TableColumn: &model.BlockContentTableColumn{},
},
})
if !s.Add(b) {
return "", fmt.Errorf("add column block")
}
return b.Model().Id, nil
}
func (t *editor) addRow(s *state.State) (string, error) {
row := makeRow(t.generateRowID())
if !s.Add(row) {
return "", fmt.Errorf("add row block")
}
return row.Model().Id, nil
}

File diff suppressed because it is too large Load diff

View file

@ -2,751 +2,28 @@ package table
import (
"fmt"
"sort"
"strings"
"github.com/globalsign/mgo/bson"
"github.com/samber/lo"
"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/simple"
"github.com/anyproto/anytype-heart/core/block/simple/table"
"github.com/anyproto/anytype-heart/core/block/simple/text"
"github.com/anyproto/anytype-heart/core/block/source"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/slice"
)
var log = logging.Logger("anytype-simple-tables")
// nolint:revive,interfacebloat
type TableEditor interface {
TableCreate(s *state.State, req pb.RpcBlockTableCreateRequest) (string, error)
RowCreate(s *state.State, req pb.RpcBlockTableRowCreateRequest) (string, error)
RowDelete(s *state.State, req pb.RpcBlockTableRowDeleteRequest) error
ColumnDelete(s *state.State, req pb.RpcBlockTableColumnDeleteRequest) error
ColumnMove(s *state.State, req pb.RpcBlockTableColumnMoveRequest) error
RowDuplicate(s *state.State, req pb.RpcBlockTableRowDuplicateRequest) (newRowID string, err error)
RowListFill(s *state.State, req pb.RpcBlockTableRowListFillRequest) error
RowListClean(s *state.State, req pb.RpcBlockTableRowListCleanRequest) error
RowSetHeader(s *state.State, req pb.RpcBlockTableRowSetHeaderRequest) error
ColumnListFill(s *state.State, req pb.RpcBlockTableColumnListFillRequest) error
cleanupTables(_ smartblock.ApplyInfo) error
ColumnCreate(s *state.State, req pb.RpcBlockTableColumnCreateRequest) (string, error)
cloneColumnStyles(s *state.State, srcColID string, targetColID string) error
ColumnDuplicate(s *state.State, req pb.RpcBlockTableColumnDuplicateRequest) (id string, err error)
Expand(s *state.State, req pb.RpcBlockTableExpandRequest) error
Sort(s *state.State, req pb.RpcBlockTableSortRequest) error
CellCreate(s *state.State, rowID string, colID string, b *model.Block) (string, error)
}
type Editor struct {
sb smartblock.SmartBlock
generateRowID func() string
generateColID func() string
}
var _ TableEditor = &Editor{}
func NewEditor(sb smartblock.SmartBlock) *Editor {
genID := func() string {
return bson.NewObjectId().Hex()
}
t := Editor{
sb: sb,
generateRowID: genID,
generateColID: genID,
}
if sb != nil {
sb.AddHook(t.cleanupTables, smartblock.HookOnBlockClose)
}
return &t
}
func (t *Editor) TableCreate(s *state.State, req pb.RpcBlockTableCreateRequest) (string, error) {
if t.sb != nil {
if err := t.sb.Restrictions().Object.Check(model.Restrictions_Blocks); err != nil {
return "", err
}
}
tableBlock := simple.New(&model.Block{
Content: &model.BlockContentOfTable{
Table: &model.BlockContentTable{},
},
})
if !s.Add(tableBlock) {
return "", fmt.Errorf("add table block")
}
if err := s.InsertTo(req.TargetId, req.Position, tableBlock.Model().Id); err != nil {
return "", fmt.Errorf("insert block: %w", err)
}
columnIds := make([]string, 0, req.Columns)
for i := uint32(0); i < req.Columns; i++ {
id, err := t.addColumnHeader(s)
if err != nil {
return "", err
}
columnIds = append(columnIds, id)
}
columnsLayout := simple.New(&model.Block{
ChildrenIds: columnIds,
Content: &model.BlockContentOfLayout{
Layout: &model.BlockContentLayout{
Style: model.BlockContentLayout_TableColumns,
},
},
})
if !s.Add(columnsLayout) {
return "", fmt.Errorf("add columns block")
}
rowIDs := make([]string, 0, req.Rows)
for i := uint32(0); i < req.Rows; i++ {
id, err := t.addRow(s)
if err != nil {
return "", err
}
rowIDs = append(rowIDs, id)
}
rowsLayout := simple.New(&model.Block{
ChildrenIds: rowIDs,
Content: &model.BlockContentOfLayout{
Layout: &model.BlockContentLayout{
Style: model.BlockContentLayout_TableRows,
},
},
})
if !s.Add(rowsLayout) {
return "", fmt.Errorf("add rows block")
}
tableBlock.Model().ChildrenIds = []string{columnsLayout.Model().Id, rowsLayout.Model().Id}
if req.WithHeaderRow {
headerID := rowIDs[0]
if err := t.RowSetHeader(s, pb.RpcBlockTableRowSetHeaderRequest{
TargetId: headerID,
IsHeader: true,
}); err != nil {
return "", fmt.Errorf("row set header: %w", err)
}
if err := t.RowListFill(s, pb.RpcBlockTableRowListFillRequest{
BlockIds: []string{headerID},
}); err != nil {
return "", fmt.Errorf("fill header row: %w", err)
}
row, err := getRow(s, headerID)
if err != nil {
return "", fmt.Errorf("get header row: %w", err)
}
for _, cellID := range row.Model().ChildrenIds {
cell := s.Get(cellID)
if cell == nil {
return "", fmt.Errorf("get header cell id %s", cellID)
}
cell.Model().BackgroundColor = "grey"
}
}
return tableBlock.Model().Id, nil
}
func (t *Editor) RowCreate(s *state.State, req pb.RpcBlockTableRowCreateRequest) (string, error) {
switch req.Position {
case model.Block_Top, model.Block_Bottom:
case model.Block_Inner:
tb, err := NewTable(s, req.TargetId)
if err != nil {
return "", fmt.Errorf("initialize table state: %w", err)
}
req.TargetId = tb.Rows().Id
default:
return "", fmt.Errorf("position is not supported")
}
rowID, err := t.addRow(s)
if err != nil {
return "", err
}
if err := s.InsertTo(req.TargetId, req.Position, rowID); err != nil {
return "", fmt.Errorf("insert row: %w", err)
}
return rowID, nil
}
func (t *Editor) RowDelete(s *state.State, req pb.RpcBlockTableRowDeleteRequest) error {
_, err := pickRow(s, req.TargetId)
if err != nil {
return fmt.Errorf("pick target row: %w", err)
}
if !s.Unlink(req.TargetId) {
return fmt.Errorf("unlink row block")
}
return nil
}
func (t *Editor) ColumnDelete(s *state.State, req pb.RpcBlockTableColumnDeleteRequest) error {
_, err := pickColumn(s, req.TargetId)
if err != nil {
return fmt.Errorf("pick target column: %w", err)
}
tb, err := NewTable(s, req.TargetId)
if err != nil {
return fmt.Errorf("initialize table state: %w", err)
}
for _, rowID := range tb.RowIDs() {
row, err := pickRow(s, rowID)
if err != nil {
return fmt.Errorf("pick row %s: %w", rowID, err)
}
for _, cellID := range row.Model().ChildrenIds {
_, colID, err := ParseCellID(cellID)
if err != nil {
return fmt.Errorf("parse cell id %s: %w", cellID, err)
}
if colID == req.TargetId {
if !s.Unlink(cellID) {
return fmt.Errorf("unlink cell %s", cellID)
}
break
}
}
}
if !s.Unlink(req.TargetId) {
return fmt.Errorf("unlink column header")
}
return nil
}
func (t *Editor) ColumnMove(s *state.State, req pb.RpcBlockTableColumnMoveRequest) error {
switch req.Position {
case model.Block_Left:
req.Position = model.Block_Top
case model.Block_Right:
req.Position = model.Block_Bottom
default:
return fmt.Errorf("position is not supported")
}
_, err := pickColumn(s, req.TargetId)
if err != nil {
return fmt.Errorf("get target column: %w", err)
}
_, err = pickColumn(s, req.DropTargetId)
if err != nil {
return fmt.Errorf("get drop target column: %w", err)
}
tb, err := NewTable(s, req.TargetId)
if err != nil {
return fmt.Errorf("init table block: %w", err)
}
if !s.Unlink(req.TargetId) {
return fmt.Errorf("unlink target column")
}
if err = s.InsertTo(req.DropTargetId, req.Position, req.TargetId); err != nil {
return fmt.Errorf("insert column: %w", err)
}
colIdx := tb.MakeColumnIndex()
for _, id := range tb.RowIDs() {
row, err := getRow(s, id)
if err != nil {
return fmt.Errorf("get row %s: %w", id, err)
}
normalizeRow(colIdx, row)
}
return nil
}
func (t *Editor) RowDuplicate(s *state.State, req pb.RpcBlockTableRowDuplicateRequest) (newRowID string, err error) {
srcRow, err := pickRow(s, req.BlockId)
if err != nil {
return "", fmt.Errorf("pick source row: %w", err)
}
newRow := srcRow.Copy()
newRow.Model().Id = t.generateRowID()
if !s.Add(newRow) {
return "", fmt.Errorf("add new row %s", newRow.Model().Id)
}
if err = s.InsertTo(req.TargetId, req.Position, newRow.Model().Id); err != nil {
return "", fmt.Errorf("insert column: %w", err)
}
for i, srcID := range newRow.Model().ChildrenIds {
cell := s.Pick(srcID)
if cell == nil {
return "", fmt.Errorf("cell %s is not found", srcID)
}
_, colID, err := ParseCellID(srcID)
if err != nil {
return "", fmt.Errorf("parse cell id %s: %w", srcID, err)
}
newCell := cell.Copy()
newCell.Model().Id = MakeCellID(newRow.Model().Id, colID)
if !s.Add(newCell) {
return "", fmt.Errorf("add new cell %s", newCell.Model().Id)
}
newRow.Model().ChildrenIds[i] = newCell.Model().Id
}
return newRow.Model().Id, nil
}
func (t *Editor) RowListFill(s *state.State, req pb.RpcBlockTableRowListFillRequest) error {
if len(req.BlockIds) == 0 {
return fmt.Errorf("empty row list")
}
tb, err := NewTable(s, req.BlockIds[0])
if err != nil {
return fmt.Errorf("init table: %w", err)
}
columns := tb.ColumnIDs()
for _, rowID := range req.BlockIds {
row, err := getRow(s, rowID)
if err != nil {
return fmt.Errorf("get row %s: %w", rowID, err)
}
newIds := make([]string, 0, len(columns))
for _, colID := range columns {
id := MakeCellID(rowID, colID)
newIds = append(newIds, id)
if !s.Exists(id) {
_, err := addCell(s, rowID, colID)
if err != nil {
return fmt.Errorf("add cell %s: %w", id, err)
}
}
}
row.Model().ChildrenIds = newIds
}
return nil
}
func (t *Editor) RowListClean(s *state.State, req pb.RpcBlockTableRowListCleanRequest) error {
if len(req.BlockIds) == 0 {
return fmt.Errorf("empty row list")
}
for _, rowID := range req.BlockIds {
row, err := pickRow(s, rowID)
if err != nil {
return fmt.Errorf("pick row: %w", err)
}
for _, cellID := range row.Model().ChildrenIds {
cell := s.Pick(cellID)
if v, ok := cell.(text.Block); ok && v.IsEmpty() {
s.Unlink(cellID)
}
}
}
return nil
}
func (t *Editor) RowSetHeader(s *state.State, req pb.RpcBlockTableRowSetHeaderRequest) error {
tb, err := NewTable(s, req.TargetId)
if err != nil {
return fmt.Errorf("init table: %w", err)
}
row, err := getRow(s, req.TargetId)
if err != nil {
return fmt.Errorf("get target row: %w", err)
}
if row.Model().GetTableRow().IsHeader != req.IsHeader {
row.Model().GetTableRow().IsHeader = req.IsHeader
err = normalizeRows(s, tb)
if err != nil {
return fmt.Errorf("normalize rows: %w", err)
}
}
return nil
}
func (t *Editor) ColumnListFill(s *state.State, req pb.RpcBlockTableColumnListFillRequest) error {
if len(req.BlockIds) == 0 {
return fmt.Errorf("empty row list")
}
tb, err := NewTable(s, req.BlockIds[0])
if err != nil {
return fmt.Errorf("init table: %w", err)
}
rows := tb.RowIDs()
for _, colID := range req.BlockIds {
for _, rowID := range rows {
id := MakeCellID(rowID, colID)
if s.Exists(id) {
continue
}
_, err := addCell(s, rowID, colID)
if err != nil {
return fmt.Errorf("add cell %s: %w", id, err)
}
row, err := getRow(s, rowID)
if err != nil {
return fmt.Errorf("get row %s: %w", rowID, err)
}
row.Model().ChildrenIds = append(row.Model().ChildrenIds, id)
}
}
colIdx := tb.MakeColumnIndex()
for _, rowID := range rows {
row, err := getRow(s, rowID)
if err != nil {
return fmt.Errorf("get row %s: %w", rowID, err)
}
normalizeRow(colIdx, row)
}
return nil
}
func (t *Editor) cleanupTables(_ smartblock.ApplyInfo) error {
if t.sb == nil {
return fmt.Errorf("nil smartblock")
}
s := t.sb.NewState()
err := s.Iterate(func(b simple.Block) bool {
if b.Model().GetTable() == nil {
return true
}
tb, err := NewTable(s, b.Model().Id)
if err != nil {
log.Errorf("cleanup: init table %s: %s", b.Model().Id, err)
return true
}
err = t.RowListClean(s, pb.RpcBlockTableRowListCleanRequest{
BlockIds: tb.RowIDs(),
})
if err != nil {
log.Errorf("cleanup table %s: %s", b.Model().Id, err)
return true
}
return true
})
if err != nil {
log.Errorf("cleanup iterate: %s", err)
}
if err = t.sb.Apply(s, smartblock.KeepInternalFlags); err != nil {
if err == source.ErrReadOnly {
return nil
}
log.Errorf("cleanup apply: %s", err)
}
return nil
}
func (t *Editor) ColumnCreate(s *state.State, req pb.RpcBlockTableColumnCreateRequest) (string, error) {
switch req.Position {
case model.Block_Left:
req.Position = model.Block_Top
if _, err := pickColumn(s, req.TargetId); err != nil {
return "", fmt.Errorf("pick column: %w", err)
}
case model.Block_Right:
req.Position = model.Block_Bottom
if _, err := pickColumn(s, req.TargetId); err != nil {
return "", fmt.Errorf("pick column: %w", err)
}
case model.Block_Inner:
tb, err := NewTable(s, req.TargetId)
if err != nil {
return "", fmt.Errorf("initialize table state: %w", err)
}
req.TargetId = tb.Columns().Id
default:
return "", fmt.Errorf("position is not supported")
}
colID, err := t.addColumnHeader(s)
if err != nil {
return "", err
}
if err = s.InsertTo(req.TargetId, req.Position, colID); err != nil {
return "", fmt.Errorf("insert column header: %w", err)
}
return colID, t.cloneColumnStyles(s, req.TargetId, colID)
}
func (t *Editor) cloneColumnStyles(s *state.State, srcColID, targetColID string) error {
tb, err := NewTable(s, srcColID)
if err != nil {
return fmt.Errorf("init table block: %w", err)
}
colIdx := tb.MakeColumnIndex()
for _, rowID := range tb.RowIDs() {
row, err := pickRow(s, rowID)
if err != nil {
return fmt.Errorf("pick row: %w", err)
}
var protoBlock simple.Block
for _, cellID := range row.Model().ChildrenIds {
_, colID, err := ParseCellID(cellID)
if err != nil {
return fmt.Errorf("parse cell id: %w", err)
}
if colID == srcColID {
protoBlock = s.Pick(cellID)
}
}
if protoBlock != nil && protoBlock.Model().BackgroundColor != "" {
targetCellID := MakeCellID(rowID, targetColID)
if !s.Exists(targetCellID) {
_, err := addCell(s, rowID, targetColID)
if err != nil {
return fmt.Errorf("add cell: %w", err)
}
}
cell := s.Get(targetCellID)
cell.Model().BackgroundColor = protoBlock.Model().BackgroundColor
row = s.Get(row.Model().Id)
row.Model().ChildrenIds = append(row.Model().ChildrenIds, targetCellID)
normalizeRow(colIdx, row)
}
}
return nil
}
func (t *Editor) ColumnDuplicate(s *state.State, req pb.RpcBlockTableColumnDuplicateRequest) (id string, err error) {
switch req.Position {
case model.Block_Left:
req.Position = model.Block_Top
case model.Block_Right:
req.Position = model.Block_Bottom
default:
return "", fmt.Errorf("position is not supported")
}
srcCol, err := pickColumn(s, req.BlockId)
if err != nil {
return "", fmt.Errorf("pick source column: %w", err)
}
_, err = pickColumn(s, req.TargetId)
if err != nil {
return "", fmt.Errorf("pick target column: %w", err)
}
tb, err := NewTable(s, req.TargetId)
if err != nil {
return "", fmt.Errorf("init table block: %w", err)
}
newCol := srcCol.Copy()
newCol.Model().Id = t.generateColID()
if !s.Add(newCol) {
return "", fmt.Errorf("add column block")
}
if err = s.InsertTo(req.TargetId, req.Position, newCol.Model().Id); err != nil {
return "", fmt.Errorf("insert column: %w", err)
}
colIdx := tb.MakeColumnIndex()
for _, rowID := range tb.RowIDs() {
row, err := getRow(s, rowID)
if err != nil {
return "", fmt.Errorf("get row %s: %w", rowID, err)
}
var cellID string
for _, id := range row.Model().ChildrenIds {
_, colID, err := ParseCellID(id)
if err != nil {
return "", fmt.Errorf("parse cell %s in row %s: %w", cellID, rowID, err)
}
if colID == req.BlockId {
cellID = id
break
}
}
if cellID == "" {
continue
}
cell := s.Pick(cellID)
if cell == nil {
return "", fmt.Errorf("cell %s is not found", cellID)
}
cell = cell.Copy()
cell.Model().Id = MakeCellID(rowID, newCol.Model().Id)
if !s.Add(cell) {
return "", fmt.Errorf("add cell block")
}
row.Model().ChildrenIds = append(row.Model().ChildrenIds, cell.Model().Id)
normalizeRow(colIdx, row)
}
return newCol.Model().Id, nil
}
func (t *Editor) Expand(s *state.State, req pb.RpcBlockTableExpandRequest) error {
tb, err := NewTable(s, req.TargetId)
if err != nil {
return fmt.Errorf("init table block: %w", err)
}
for i := uint32(0); i < req.Columns; i++ {
_, err := t.ColumnCreate(s, pb.RpcBlockTableColumnCreateRequest{
TargetId: req.TargetId,
Position: model.Block_Inner,
})
if err != nil {
return fmt.Errorf("create column: %w", err)
}
}
for i := uint32(0); i < req.Rows; i++ {
rows := tb.Rows()
_, err := t.RowCreate(s, pb.RpcBlockTableRowCreateRequest{
TargetId: rows.ChildrenIds[len(rows.ChildrenIds)-1],
Position: model.Block_Bottom,
})
if err != nil {
return fmt.Errorf("create row: %w", err)
}
}
return nil
}
func (t *Editor) Sort(s *state.State, req pb.RpcBlockTableSortRequest) error {
_, err := pickColumn(s, req.ColumnId)
if err != nil {
return fmt.Errorf("pick column: %w", err)
}
tb, err := NewTable(s, req.ColumnId)
if err != nil {
return fmt.Errorf("init table block: %w", err)
}
rows := s.Get(tb.Rows().Id)
sorter := tableSorter{
rowIDs: make([]string, 0, len(rows.Model().ChildrenIds)),
values: make([]string, len(rows.Model().ChildrenIds)),
}
var headers []string
var i int
for _, rowID := range rows.Model().ChildrenIds {
row, err := pickRow(s, rowID)
if err != nil {
return fmt.Errorf("pick row %s: %w", rowID, err)
}
if row.Model().GetTableRow().GetIsHeader() {
headers = append(headers, rowID)
continue
}
sorter.rowIDs = append(sorter.rowIDs, rowID)
for _, cellID := range row.Model().ChildrenIds {
_, colID, err := ParseCellID(cellID)
if err != nil {
return fmt.Errorf("parse cell id %s: %w", cellID, err)
}
if colID == req.ColumnId {
cell := s.Pick(cellID)
if cell == nil {
return fmt.Errorf("cell %s is not found", cellID)
}
sorter.values[i] = cell.Model().GetText().GetText()
}
}
i++
}
if req.Type == model.BlockContentDataviewSort_Asc {
sort.Stable(sorter)
} else {
sort.Stable(sort.Reverse(sorter))
}
// nolint:gocritic
rows.Model().ChildrenIds = append(headers, sorter.rowIDs...)
return nil
}
func (t *Editor) CellCreate(s *state.State, rowID string, colID string, b *model.Block) (string, error) {
tb, err := NewTable(s, rowID)
if err != nil {
return "", fmt.Errorf("initialize table state: %w", err)
}
row, err := getRow(s, rowID)
if err != nil {
return "", fmt.Errorf("get row: %w", err)
}
if _, err = pickColumn(s, colID); err != nil {
return "", fmt.Errorf("pick column: %w", err)
}
cellID, err := addCell(s, rowID, colID)
if err != nil {
return "", fmt.Errorf("add cell: %w", err)
}
cell := s.Get(cellID)
cell.Model().Content = b.Content
if err := s.InsertTo(rowID, model.Block_Inner, cellID); err != nil {
return "", fmt.Errorf("insert to: %w", err)
}
colIdx := tb.MakeColumnIndex()
normalizeRow(colIdx, row)
return cellID, nil
}
var ErrCannotMoveTableBlocks = fmt.Errorf("can not move table blocks")
var (
errNotARow = fmt.Errorf("block is not a row")
errNotAColumn = fmt.Errorf("block is not a column")
errRowNotFound = fmt.Errorf("row is not found")
errColumnNotFound = fmt.Errorf("column is not found")
)
type tableSorter struct {
rowIDs []string
@ -766,27 +43,6 @@ func (t tableSorter) Swap(i, j int) {
t.rowIDs[i], t.rowIDs[j] = t.rowIDs[j], t.rowIDs[i]
}
func (t *Editor) addColumnHeader(s *state.State) (string, error) {
b := simple.New(&model.Block{
Id: t.generateColID(),
Content: &model.BlockContentOfTableColumn{
TableColumn: &model.BlockContentTableColumn{},
},
})
if !s.Add(b) {
return "", fmt.Errorf("add column block")
}
return b.Model().Id, nil
}
func (t *Editor) addRow(s *state.State) (string, error) {
row := makeRow(t.generateRowID())
if !s.Add(row) {
return "", fmt.Errorf("add row block")
}
return row.Model().Id, nil
}
func makeRow(id string) simple.Block {
return simple.New(&model.Block{
Id: id,
@ -799,11 +55,11 @@ func makeRow(id string) simple.Block {
func getRow(s *state.State, id string) (simple.Block, error) {
b := s.Get(id)
if b == nil {
return nil, fmt.Errorf("row is not found")
return nil, errRowNotFound
}
_, ok := b.(table.RowBlock)
if !ok {
return nil, fmt.Errorf("block is not a row")
return nil, errNotARow
}
return b, nil
}
@ -811,21 +67,30 @@ func getRow(s *state.State, id string) (simple.Block, error) {
func pickRow(s *state.State, id string) (simple.Block, error) {
b := s.Pick(id)
if b == nil {
return nil, fmt.Errorf("row is not found")
return nil, errRowNotFound
}
if b.Model().GetTableRow() == nil {
return nil, fmt.Errorf("block is not a row")
return nil, errNotARow
}
return b, nil
}
func makeColumn(id string) simple.Block {
return simple.New(&model.Block{
Id: id,
Content: &model.BlockContentOfTableColumn{
TableColumn: &model.BlockContentTableColumn{},
},
})
}
func pickColumn(s *state.State, id string) (simple.Block, error) {
b := s.Pick(id)
if b == nil {
return nil, fmt.Errorf("block is not found")
return nil, errColumnNotFound
}
if b.Model().GetTableColumn() == nil {
return nil, fmt.Errorf("block is not a column")
return nil, errNotAColumn
}
return b, nil
}
@ -868,14 +133,7 @@ func NewTable(s *state.State, id string) (*Table, error) {
s: s,
}
next := s.Pick(id)
for next != nil {
if next.Model().GetTable() != nil {
tb.block = next
break
}
next = s.PickParentOf(next.Model().Id)
}
tb.block = PickTableRootBlock(s, id)
if tb.block == nil {
return nil, fmt.Errorf("root table block is not found")
}
@ -901,6 +159,19 @@ func NewTable(s *state.State, id string) (*Table, error) {
return &tb, nil
}
// PickTableRootBlock iterates over parents of block. Returns nil in case root table block is not found
func PickTableRootBlock(s *state.State, id string) (block simple.Block) {
next := s.Pick(id)
for next != nil {
if next.Model().GetTable() != nil {
block = next
break
}
next = s.PickParentOf(next.Model().Id)
}
return block
}
// destructureDivs removes child dividers from block
func destructureDivs(s *state.State, blockID string) {
parent := s.Pick(blockID)
@ -1006,3 +277,48 @@ func (tb Table) Iterate(f func(b simple.Block, pos CellPosition) bool) error {
}
return nil
}
func (tb Table) MoveBlocksUnderTheTable(ids ...string) {
parent := tb.s.GetParentOf(tb.block.Model().Id)
if parent == nil {
log.Errorf("failed to get parent of table block '%s'", tb.block.Model().Id)
return
}
children := parent.Model().ChildrenIds
pos := slice.FindPos(children, tb.block.Model().Id)
if pos == -1 {
log.Errorf("failed to find table block '%s' among children of block '%s'", tb.block.Model().Id, parent.Model().Id)
return
}
tb.s.RemoveFromCache(ids)
tb.s.SetChildrenIds(parent.Model(), slice.Insert(children, pos+1, ids...))
}
// CheckTableBlocksMove checks if Insert operation is allowed in case table blocks are affected
func CheckTableBlocksMove(st *state.State, target string, pos model.BlockPosition, blockIds []string) (string, model.BlockPosition, error) {
if t, err := NewTable(st, target); err == nil && t != nil {
// we allow moving rows between each other
if lo.Every(t.RowIDs(), append(blockIds, target)) {
if pos == model.Block_Bottom || pos == model.Block_Top {
return target, pos, nil
}
return "", 0, fmt.Errorf("failed to move rows: position should be Top or Bottom, got %s", model.BlockPosition_name[int32(pos)])
}
}
for _, id := range blockIds {
t := PickTableRootBlock(st, id)
if t != nil && t.Model().Id != id {
// we should not move table blocks except table root block
return "", 0, ErrCannotMoveTableBlocks
}
}
t := PickTableRootBlock(st, target)
if t != nil && t.Model().Id != target {
// if the target is one of table blocks, but not table root, we should insert blocks under the table
return t.Model().Id, model.Block_Bottom, nil
}
return target, pos, nil
}

File diff suppressed because it is too large Load diff

View file

@ -81,20 +81,12 @@ var WithNoDuplicateLinks = func() StateTransformer {
var WithRelations = func(rels []domain.RelationKey) StateTransformer {
return func(s *state.State) {
var links []*model.RelationLink
for _, relKey := range rels {
if s.HasRelation(relKey.String()) {
continue
}
rel := bundle.MustGetRelation(relKey)
links = append(links, &model.RelationLink{Format: rel.Format, Key: rel.Key})
}
s.AddRelationLinks(links...)
s.AddBundledRelationLinks(rels...)
}
}
var WithRequiredRelations = func() StateTransformer {
return WithRelations(bundle.RequiredInternalRelations)
var WithRequiredRelations = func(s *state.State) {
WithRelations(bundle.RequiredInternalRelations)(s)
}
var WithObjectTypesAndLayout = func(otypes []domain.TypeKey, layout model.ObjectTypeLayout) StateTransformer {
@ -333,7 +325,6 @@ var WithDescription = func(s *state.State) {
}
var WithNoTitle = StateTransformer(func(s *state.State) {
WithFirstTextBlock(s)
s.Unlink(TitleBlockId)
})
@ -622,7 +613,7 @@ var WithBookmarkBlocks = func(s *state.State) {
for _, k := range bookmarkRelationKeys {
if !s.HasRelation(k) {
s.AddBundledRelations(domain.RelationKey(k))
s.AddBundledRelationLinks(domain.RelationKey(k))
}
}

View file

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

View file

@ -16,6 +16,10 @@ import (
"github.com/anyproto/anytype-heart/util/pbtypes"
)
var workspaceRequiredRelations = []domain.RelationKey{
// SpaceInviteFileCid and SpaceInviteFileKey are added only when creating invite
}
type Workspaces struct {
smartblock.SmartBlock
basic.AllOperations
@ -31,7 +35,7 @@ type Workspaces struct {
func (f *ObjectFactory) newWorkspace(sb smartblock.SmartBlock) *Workspaces {
w := &Workspaces{
SmartBlock: sb,
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter),
AllOperations: basic.NewBasic(sb, f.objectStore, f.layoutConverter, f.fileObjectService),
IHistory: basic.NewHistory(sb),
Text: stext.NewText(
sb,
@ -49,6 +53,7 @@ func (f *ObjectFactory) newWorkspace(sb smartblock.SmartBlock) *Workspaces {
}
func (w *Workspaces) Init(ctx *smartblock.InitContext) (err error) {
ctx.RequiredInternalRelationKeys = append(ctx.RequiredInternalRelationKeys, workspaceRequiredRelations...)
err = w.SmartBlock.Init(ctx)
if err != nil {
return err

View file

@ -697,7 +697,7 @@ func (e *export) getRelatedDerivedObjects(objects map[string]*types.Struct) ([]d
if len(typesAndTemplates) > 0 {
derivedObjectsMap := make(map[string]*types.Struct, 0)
for _, object := range typesAndTemplates {
id := object.Get(bundle.RelationKeyId.String()).GetStringValue()
id := pbtypes.GetString(object.Details, bundle.RelationKeyId.String())
derivedObjectsMap[id] = object.Details
}
iteratedObjects, typesAndTemplates, err := e.iterateObjects(derivedObjectsMap)
@ -890,8 +890,8 @@ func (e *export) addRelationAndOptions(relation *database.Record, derivedObjects
}
func (e *export) addRelation(relation database.Record, derivedObjects []database.Record) []database.Record {
if relationKey := relation.Get(bundle.RelationKeyRelationKey.String()); relationKey != nil {
if !bundle.HasRelation(relationKey.GetStringValue()) {
if relationKey := pbtypes.GetString(relation.Details, bundle.RelationKeyRelationKey.String()); relationKey != "" {
if !bundle.HasRelation(relationKey) {
derivedObjects = append(derivedObjects, relation)
}
}

View file

@ -51,10 +51,12 @@ func Test_docsForExport(t *testing.T) {
storeFixture := objectstore.NewStoreFixture(t)
storeFixture.AddObjects(t, []objectstore.TestObject{
{
bundle.RelationKeyId: pbtypes.String("id"),
bundle.RelationKeyId: pbtypes.String("id"),
bundle.RelationKeyName: pbtypes.String("name1"),
},
{
bundle.RelationKeyId: pbtypes.String("id1"),
bundle.RelationKeyId: pbtypes.String("id1"),
bundle.RelationKeyName: pbtypes.String("name2"),
},
})
err := storeFixture.UpdateObjectLinks("id", []string{"id1"})
@ -83,7 +85,8 @@ func Test_docsForExport(t *testing.T) {
storeFixture := objectstore.NewStoreFixture(t)
storeFixture.AddObjects(t, []objectstore.TestObject{
{
bundle.RelationKeyId: pbtypes.String("id"),
bundle.RelationKeyId: pbtypes.String("id"),
bundle.RelationKeyName: pbtypes.String("name"),
},
{
bundle.RelationKeyId: pbtypes.String("id1"),

View file

@ -111,6 +111,7 @@ func (oc *ObjectCreator) Create(dataObject *DataObject, sn *common.Snapshot) (*t
log.With("objectID", newID).Errorf("failed to update objects ids: %s", err)
}
oc.updateKeys(st, oldIDtoNew)
if sn.SbType == coresb.SmartBlockTypeWorkspace {
oc.setSpaceDashboardID(spaceID, st)
return nil, newID, nil
@ -255,7 +256,9 @@ func (oc *ObjectCreator) createNewObject(
}
})
if err == nil {
sb.Lock()
respDetails = sb.Details()
sb.Unlock()
} else if errors.Is(err, treestorage.ErrTreeExists) {
err = spc.Do(newID, func(sb smartblock.SmartBlock) error {
respDetails = sb.Details()
@ -535,3 +538,16 @@ func (oc *ObjectCreator) getExistingWidgetsTargetIDs(oldState *state.State) (map
}
return existingWidgetsTargetIDs, nil
}
func (oc *ObjectCreator) updateKeys(st *state.State, oldIDtoNew map[string]string) {
for key, value := range st.Details().GetFields() {
if newKey, ok := oldIDtoNew[key]; ok {
st.SetDetail(newKey, value)
st.RemoveRelation(key)
}
}
if newKey, ok := oldIDtoNew[st.ObjectTypeKey().String()]; ok {
st.SetObjectTypeKey(domain.TypeKey(newKey))
}
}

View file

@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/assert"
"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/block/import/common"
"github.com/anyproto/anytype-heart/core/block/import/common/objectcreator/mock_blockservice"
"github.com/anyproto/anytype-heart/core/block/object/objectcreator"
@ -84,3 +85,47 @@ func TestObjectCreator_Create(t *testing.T) {
assert.Equal(t, testDetails, testParticipant.CombinedDetails())
})
}
func TestObjectCreator_updateKeys(t *testing.T) {
t.Run("updateKeys - update relation key", func(t *testing.T) {
// given
oc := ObjectCreator{}
oldToNew := map[string]string{"oldId": "newId", "oldKey": "newKey"}
doc := state.NewDoc("oldId", nil).(*state.State)
doc.SetDetails(&types.Struct{Fields: map[string]*types.Value{
"oldKey": pbtypes.String("test"),
}})
// when
oc.updateKeys(doc, oldToNew)
// then
assert.Nil(t, doc.Details().GetFields()["oldKey"])
assert.Equal(t, pbtypes.String("test"), doc.Details().GetFields()["newKey"])
})
t.Run("updateKeys - update object type key", func(t *testing.T) {
// given
oc := ObjectCreator{}
oldToNew := map[string]string{"oldId": "newId", "oldKey": "newKey"}
doc := state.NewDoc("oldId", nil).(*state.State)
doc.SetObjectTypeKey("oldKey")
// when
oc.updateKeys(doc, oldToNew)
// then
assert.Equal(t, domain.TypeKey("newKey"), doc.ObjectTypeKey())
})
t.Run("nothing to update - update object type key", func(t *testing.T) {
// given
oc := ObjectCreator{}
oldToNew := map[string]string{"oldId": "newId", "oldKey": "newKey"}
doc := state.NewDoc("oldId", nil).(*state.State)
// when
oc.updateKeys(doc, oldToNew)
// then
assert.Nil(t, doc.Details().GetFields()["newKey"])
assert.Equal(t, domain.TypeKey(""), doc.ObjectTypeKey())
})
}

View file

@ -6,12 +6,17 @@ import (
"time"
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
"github.com/globalsign/mgo/bson"
"github.com/anyproto/anytype-heart/core/block/import/common"
"github.com/anyproto/anytype-heart/core/block/object/payloadcreator"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/domain/objectorigin"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
sb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/database"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/space"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
@ -19,14 +24,16 @@ import (
type derivedObject struct {
existingObject *existingObject
spaceService space.Service
objectStore objectstore.ObjectStore
internalKey string
}
func newDerivedObject(existingObject *existingObject, spaceService space.Service) *derivedObject {
return &derivedObject{existingObject: existingObject, spaceService: spaceService}
func newDerivedObject(existingObject *existingObject, spaceService space.Service, objectStore objectstore.ObjectStore) *derivedObject {
return &derivedObject{existingObject: existingObject, spaceService: spaceService, objectStore: objectStore}
}
func (r *derivedObject) GetIDAndPayload(ctx context.Context, spaceID string, sn *common.Snapshot, _ time.Time, getExisting bool, _ objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error) {
id, payload, err := r.existingObject.GetIDAndPayload(ctx, spaceID, sn, getExisting)
func (d *derivedObject) GetIDAndPayload(ctx context.Context, spaceID string, sn *common.Snapshot, _ time.Time, getExisting bool, _ objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error) {
id, payload, err := d.existingObject.GetIDAndPayload(ctx, spaceID, sn, getExisting)
if err != nil {
return "", treestorage.TreeStorageCreatePayload{}, err
}
@ -42,7 +49,16 @@ func (r *derivedObject) GetIDAndPayload(ctx context.Context, spaceID string, sn
}
}
spc, err := r.spaceService.Get(ctx, spaceID)
var key string
if d.isDeletedObject(spaceID, uniqueKey.Marshal()) {
key = bson.NewObjectId().Hex()
uniqueKey, err = domain.NewUniqueKey(sn.SbType, key)
if err != nil {
return "", treestorage.TreeStorageCreatePayload{}, fmt.Errorf("create unique key from %s: %w", sn.SbType, err)
}
}
d.internalKey = key
spc, err := d.spaceService.Get(ctx, spaceID)
if err != nil {
return "", treestorage.TreeStorageCreatePayload{}, fmt.Errorf("get space : %w", err)
}
@ -54,3 +70,29 @@ func (r *derivedObject) GetIDAndPayload(ctx context.Context, spaceID string, sn
}
return payload.RootRawChange.Id, payload, nil
}
func (d *derivedObject) GetInternalKey(sbType sb.SmartBlockType) string {
return d.internalKey
}
func (d *derivedObject) isDeletedObject(spaceId string, uniqueKey string) bool {
ids, _, err := d.objectStore.QueryObjectIDs(database.Query{
Filters: []*model.BlockContentDataviewFilter{
{
Condition: model.BlockContentDataviewFilter_Equal,
RelationKey: bundle.RelationKeySpaceId.String(),
Value: pbtypes.String(spaceId),
},
{
Condition: model.BlockContentDataviewFilter_Equal,
RelationKey: bundle.RelationKeyUniqueKey.String(),
Value: pbtypes.String(uniqueKey),
},
{
Condition: model.BlockContentDataviewFilter_Equal,
RelationKey: bundle.RelationKeyIsDeleted.String(),
Value: pbtypes.Bool(true),
},
},
})
return err == nil && len(ids) > 0
}

View file

@ -0,0 +1,69 @@
package objectid
import (
"context"
"testing"
"time"
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/anyproto/anytype-heart/core/block/import/common"
"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"
coresb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/space/clientspace/mock_clientspace"
"github.com/anyproto/anytype-heart/space/mock_space"
"github.com/anyproto/anytype-heart/util/pbtypes"
)
func TestDerivedObject_GetIDAndPayload(t *testing.T) {
t.Run("try to recreate deleted object", func(t *testing.T) {
// given
sf := objectstore.NewStoreFixture(t)
service := mock_space.NewMockService(t)
deriveObject := newDerivedObject(newExistingObject(sf), service, sf)
sn := &common.Snapshot{
Id: "oldId",
Snapshot: &pb.ChangeSnapshot{
Data: &model.SmartBlockSnapshotBase{
Details: &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyUniqueKey.String(): pbtypes.String("key"),
}},
Key: "oldKey",
},
},
SbType: coresb.SmartBlockTypePage,
}
space := mock_clientspace.NewMockSpace(t)
service.EXPECT().Get(context.Background(), "spaceId").Return(space, nil)
space.EXPECT().DeriveTreePayload(context.Background(), mock.Anything).Return(treestorage.TreeStorageCreatePayload{
RootRawChange: &treechangeproto.RawTreeChangeWithId{Id: "newId"},
}, nil)
uniqueKey, err := domain.NewUniqueKey(coresb.SmartBlockTypePage, "oldKey")
assert.Nil(t, err)
sf.AddObjects(t, []objectstore.TestObject{
{
bundle.RelationKeyUniqueKey: pbtypes.String(uniqueKey.Marshal()),
bundle.RelationKeyId: pbtypes.String("oldId"),
bundle.RelationKeyIsDeleted: pbtypes.Bool(true),
},
})
// when
id, _, err := deriveObject.GetIDAndPayload(context.Background(), "spaceId", sn, time.Now(), false, objectorigin.Import(model.Import_Pb))
// then
assert.Nil(t, err)
assert.NotEqual(t, deriveObject.GetInternalKey(sn.SbType), "key")
assert.Equal(t, "newId", id)
})
}

View file

@ -1,103 +0,0 @@
// Code generated by mockery v2.30.1. DO NOT EDIT.
package mock_objectid
import (
"context"
time "time"
treestorage "github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
mock "github.com/stretchr/testify/mock"
"github.com/anyproto/anytype-heart/core/block/import/common"
"github.com/anyproto/anytype-heart/core/domain/objectorigin"
)
// MockIDGetter is an autogenerated mock type for the IDGetter type
type MockIDGetter struct {
mock.Mock
}
type MockIDGetter_Expecter struct {
mock *mock.Mock
}
func (_m *MockIDGetter) EXPECT() *MockIDGetter_Expecter {
return &MockIDGetter_Expecter{mock: &_m.Mock}
}
// GetID provides a mock function with given fields: spaceID, sn, createdTime, getExisting
func (_m *MockIDGetter) GetIDAndPayload(ctx context.Context, spaceID string, sn *common.Snapshot, createdTime time.Time, getExisting bool, origin objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error) {
ret := _m.Called(spaceID, sn, createdTime, getExisting)
var r0 string
var r1 treestorage.TreeStorageCreatePayload
var r2 error
if rf, ok := ret.Get(0).(func(string, *common.Snapshot, time.Time, bool) (string, treestorage.TreeStorageCreatePayload, error)); ok {
return rf(spaceID, sn, createdTime, getExisting)
}
if rf, ok := ret.Get(0).(func(string, *common.Snapshot, time.Time, bool) string); ok {
r0 = rf(spaceID, sn, createdTime, getExisting)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(string, *common.Snapshot, time.Time, bool) treestorage.TreeStorageCreatePayload); ok {
r1 = rf(spaceID, sn, createdTime, getExisting)
} else {
r1 = ret.Get(1).(treestorage.TreeStorageCreatePayload)
}
if rf, ok := ret.Get(2).(func(string, *common.Snapshot, time.Time, bool) error); ok {
r2 = rf(spaceID, sn, createdTime, getExisting)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// MockIDGetter_GetID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIDAndPayload'
type MockIDGetter_GetID_Call struct {
*mock.Call
}
// GetID is a helper method to define mock.On call
// - spaceID string
// - sn *common.Snapshot
// - createdTime time.Time
// - getExisting bool
func (_e *MockIDGetter_Expecter) GetID(spaceID interface{}, sn interface{}, createdTime interface{}, getExisting interface{}) *MockIDGetter_GetID_Call {
return &MockIDGetter_GetID_Call{Call: _e.mock.On("GetIDAndPayload", spaceID, sn, createdTime, getExisting)}
}
func (_c *MockIDGetter_GetID_Call) Run(run func(spaceID string, sn *common.Snapshot, createdTime time.Time, getExisting bool)) *MockIDGetter_GetID_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(*common.Snapshot), args[2].(time.Time), args[3].(bool))
})
return _c
}
func (_c *MockIDGetter_GetID_Call) Return(_a0 string, _a1 treestorage.TreeStorageCreatePayload, _a2 error) *MockIDGetter_GetID_Call {
_c.Call.Return(_a0, _a1, _a2)
return _c
}
func (_c *MockIDGetter_GetID_Call) RunAndReturn(run func(string, *common.Snapshot, time.Time, bool) (string, treestorage.TreeStorageCreatePayload, error)) *MockIDGetter_GetID_Call {
_c.Call.Return(run)
return _c
}
// NewMockIDGetter creates a new instance of MockIDGetter. 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 NewMockIDGetter(t interface {
mock.TestingT
Cleanup(func())
}) *MockIDGetter {
mock := &MockIDGetter{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -1,110 +0,0 @@
// Code generated by mockery. DO NOT EDIT.
package mock_objectid
import (
context "context"
common "github.com/anyproto/anytype-heart/core/block/import/common"
"github.com/anyproto/anytype-heart/core/domain/objectorigin"
mock "github.com/stretchr/testify/mock"
time "time"
treestorage "github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
)
// MockIDProvider is an autogenerated mock type for the IDProvider type
type MockIDProvider struct {
mock.Mock
}
type MockIDProvider_Expecter struct {
mock *mock.Mock
}
func (_m *MockIDProvider) EXPECT() *MockIDProvider_Expecter {
return &MockIDProvider_Expecter{mock: &_m.Mock}
}
// GetIDAndPayload provides a mock function with given fields: ctx, spaceID, sn, createdTime, getExisting
func (_m *MockIDProvider) GetIDAndPayload(ctx context.Context, spaceID string, sn *common.Snapshot, createdTime time.Time, getExisting bool, origin objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error) {
ret := _m.Called(ctx, spaceID, sn, createdTime, getExisting)
if len(ret) == 0 {
panic("no return value specified for GetIDAndPayload")
}
var r0 string
var r1 treestorage.TreeStorageCreatePayload
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, *common.Snapshot, time.Time, bool) (string, treestorage.TreeStorageCreatePayload, error)); ok {
return rf(ctx, spaceID, sn, createdTime, getExisting)
}
if rf, ok := ret.Get(0).(func(context.Context, string, *common.Snapshot, time.Time, bool) string); ok {
r0 = rf(ctx, spaceID, sn, createdTime, getExisting)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(context.Context, string, *common.Snapshot, time.Time, bool) treestorage.TreeStorageCreatePayload); ok {
r1 = rf(ctx, spaceID, sn, createdTime, getExisting)
} else {
r1 = ret.Get(1).(treestorage.TreeStorageCreatePayload)
}
if rf, ok := ret.Get(2).(func(context.Context, string, *common.Snapshot, time.Time, bool) error); ok {
r2 = rf(ctx, spaceID, sn, createdTime, getExisting)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// MockIDProvider_GetIDAndPayload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIDAndPayload'
type MockIDProvider_GetIDAndPayload_Call struct {
*mock.Call
}
// GetIDAndPayload is a helper method to define mock.On call
// - ctx context.Context
// - spaceID string
// - sn *common.Snapshot
// - createdTime time.Time
// - getExisting bool
func (_e *MockIDProvider_Expecter) GetIDAndPayload(ctx interface{}, spaceID interface{}, sn interface{}, createdTime interface{}, getExisting interface{}) *MockIDProvider_GetIDAndPayload_Call {
return &MockIDProvider_GetIDAndPayload_Call{Call: _e.mock.On("GetIDAndPayload", ctx, spaceID, sn, createdTime, getExisting)}
}
func (_c *MockIDProvider_GetIDAndPayload_Call) Run(run func(ctx context.Context, spaceID string, sn *common.Snapshot, createdTime time.Time, getExisting bool)) *MockIDProvider_GetIDAndPayload_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(*common.Snapshot), args[3].(time.Time), args[4].(bool))
})
return _c
}
func (_c *MockIDProvider_GetIDAndPayload_Call) Return(_a0 string, _a1 treestorage.TreeStorageCreatePayload, _a2 error) *MockIDProvider_GetIDAndPayload_Call {
_c.Call.Return(_a0, _a1, _a2)
return _c
}
func (_c *MockIDProvider_GetIDAndPayload_Call) RunAndReturn(run func(context.Context, string, *common.Snapshot, time.Time, bool) (string, treestorage.TreeStorageCreatePayload, error)) *MockIDProvider_GetIDAndPayload_Call {
_c.Call.Return(run)
return _c
}
// NewMockIDProvider creates a new instance of MockIDProvider. 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 NewMockIDProvider(t interface {
mock.TestingT
Cleanup(func())
}) *MockIDProvider {
mock := &MockIDProvider{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -0,0 +1,160 @@
// Code generated by mockery. DO NOT EDIT.
package mock_objectid
import (
context "context"
common "github.com/anyproto/anytype-heart/core/block/import/common"
mock "github.com/stretchr/testify/mock"
objectorigin "github.com/anyproto/anytype-heart/core/domain/objectorigin"
smartblock "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
time "time"
treestorage "github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
)
// MockIdAndKeyProvider is an autogenerated mock type for the IdAndKeyProvider type
type MockIdAndKeyProvider struct {
mock.Mock
}
type MockIdAndKeyProvider_Expecter struct {
mock *mock.Mock
}
func (_m *MockIdAndKeyProvider) EXPECT() *MockIdAndKeyProvider_Expecter {
return &MockIdAndKeyProvider_Expecter{mock: &_m.Mock}
}
// GetIDAndPayload provides a mock function with given fields: ctx, spaceID, sn, createdTime, getExisting, origin
func (_m *MockIdAndKeyProvider) GetIDAndPayload(ctx context.Context, spaceID string, sn *common.Snapshot, createdTime time.Time, getExisting bool, origin objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error) {
ret := _m.Called(ctx, spaceID, sn, createdTime, getExisting, origin)
if len(ret) == 0 {
panic("no return value specified for GetIDAndPayload")
}
var r0 string
var r1 treestorage.TreeStorageCreatePayload
var r2 error
if rf, ok := ret.Get(0).(func(context.Context, string, *common.Snapshot, time.Time, bool, objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error)); ok {
return rf(ctx, spaceID, sn, createdTime, getExisting, origin)
}
if rf, ok := ret.Get(0).(func(context.Context, string, *common.Snapshot, time.Time, bool, objectorigin.ObjectOrigin) string); ok {
r0 = rf(ctx, spaceID, sn, createdTime, getExisting, origin)
} else {
r0 = ret.Get(0).(string)
}
if rf, ok := ret.Get(1).(func(context.Context, string, *common.Snapshot, time.Time, bool, objectorigin.ObjectOrigin) treestorage.TreeStorageCreatePayload); ok {
r1 = rf(ctx, spaceID, sn, createdTime, getExisting, origin)
} else {
r1 = ret.Get(1).(treestorage.TreeStorageCreatePayload)
}
if rf, ok := ret.Get(2).(func(context.Context, string, *common.Snapshot, time.Time, bool, objectorigin.ObjectOrigin) error); ok {
r2 = rf(ctx, spaceID, sn, createdTime, getExisting, origin)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// MockIdAndKeyProvider_GetIDAndPayload_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIDAndPayload'
type MockIdAndKeyProvider_GetIDAndPayload_Call struct {
*mock.Call
}
// GetIDAndPayload is a helper method to define mock.On call
// - ctx context.Context
// - spaceID string
// - sn *common.Snapshot
// - createdTime time.Time
// - getExisting bool
// - origin objectorigin.ObjectOrigin
func (_e *MockIdAndKeyProvider_Expecter) GetIDAndPayload(ctx interface{}, spaceID interface{}, sn interface{}, createdTime interface{}, getExisting interface{}, origin interface{}) *MockIdAndKeyProvider_GetIDAndPayload_Call {
return &MockIdAndKeyProvider_GetIDAndPayload_Call{Call: _e.mock.On("GetIDAndPayload", ctx, spaceID, sn, createdTime, getExisting, origin)}
}
func (_c *MockIdAndKeyProvider_GetIDAndPayload_Call) Run(run func(ctx context.Context, spaceID string, sn *common.Snapshot, createdTime time.Time, getExisting bool, origin objectorigin.ObjectOrigin)) *MockIdAndKeyProvider_GetIDAndPayload_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(*common.Snapshot), args[3].(time.Time), args[4].(bool), args[5].(objectorigin.ObjectOrigin))
})
return _c
}
func (_c *MockIdAndKeyProvider_GetIDAndPayload_Call) Return(_a0 string, _a1 treestorage.TreeStorageCreatePayload, _a2 error) *MockIdAndKeyProvider_GetIDAndPayload_Call {
_c.Call.Return(_a0, _a1, _a2)
return _c
}
func (_c *MockIdAndKeyProvider_GetIDAndPayload_Call) RunAndReturn(run func(context.Context, string, *common.Snapshot, time.Time, bool, objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error)) *MockIdAndKeyProvider_GetIDAndPayload_Call {
_c.Call.Return(run)
return _c
}
// GetInternalKey provides a mock function with given fields: sbType
func (_m *MockIdAndKeyProvider) GetInternalKey(sbType smartblock.SmartBlockType) string {
ret := _m.Called(sbType)
if len(ret) == 0 {
panic("no return value specified for GetInternalKey")
}
var r0 string
if rf, ok := ret.Get(0).(func(smartblock.SmartBlockType) string); ok {
r0 = rf(sbType)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockIdAndKeyProvider_GetInternalKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetInternalKey'
type MockIdAndKeyProvider_GetInternalKey_Call struct {
*mock.Call
}
// GetInternalKey is a helper method to define mock.On call
// - sbType smartblock.SmartBlockType
func (_e *MockIdAndKeyProvider_Expecter) GetInternalKey(sbType interface{}) *MockIdAndKeyProvider_GetInternalKey_Call {
return &MockIdAndKeyProvider_GetInternalKey_Call{Call: _e.mock.On("GetInternalKey", sbType)}
}
func (_c *MockIdAndKeyProvider_GetInternalKey_Call) Run(run func(sbType smartblock.SmartBlockType)) *MockIdAndKeyProvider_GetInternalKey_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(smartblock.SmartBlockType))
})
return _c
}
func (_c *MockIdAndKeyProvider_GetInternalKey_Call) Return(_a0 string) *MockIdAndKeyProvider_GetInternalKey_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockIdAndKeyProvider_GetInternalKey_Call) RunAndReturn(run func(smartblock.SmartBlockType) string) *MockIdAndKeyProvider_GetInternalKey_Call {
_c.Call.Return(run)
return _c
}
// NewMockIdAndKeyProvider creates a new instance of MockIdAndKeyProvider. 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 NewMockIdAndKeyProvider(t interface {
mock.TestingT
Cleanup(func())
}) *MockIdAndKeyProvider {
mock := &MockIdAndKeyProvider{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View file

@ -20,10 +20,19 @@ import (
var log = logging.Logger("import").Desugar()
type IdAndKeyProvider interface {
IDProvider
InternalKeyProvider
}
type IDProvider interface {
GetIDAndPayload(ctx context.Context, spaceID string, sn *common.Snapshot, createdTime time.Time, getExisting bool, origin objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error)
}
type InternalKeyProvider interface {
GetInternalKey(sbType sb.SmartBlockType) string
}
type Provider struct {
idProviderBySmartBlockType map[sb.SmartBlockType]IDProvider
}
@ -34,13 +43,13 @@ func NewIDProvider(
blockService *block.Service,
fileStore filestore.FileStore,
fileObjectService fileobject.Service,
) IDProvider {
) IdAndKeyProvider {
p := &Provider{
idProviderBySmartBlockType: make(map[sb.SmartBlockType]IDProvider, 0),
}
existingObject := newExistingObject(objectStore)
treeObject := newTreeObject(existingObject, spaceService)
derivedObject := newDerivedObject(existingObject, spaceService)
derivedObject := newDerivedObject(existingObject, spaceService, objectStore)
fileObject := &fileObject{
treeObject: treeObject,
blockService: blockService,
@ -64,9 +73,25 @@ func NewIDProvider(
return p
}
func (p *Provider) GetIDAndPayload(ctx context.Context, spaceID string, sn *common.Snapshot, createdTime time.Time, getExisting bool, origin objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error) {
func (p *Provider) GetIDAndPayload(
ctx context.Context,
spaceID string,
sn *common.Snapshot,
createdTime time.Time,
getExisting bool,
origin objectorigin.ObjectOrigin,
) (string, treestorage.TreeStorageCreatePayload, error) {
if idProvider, ok := p.idProviderBySmartBlockType[sn.SbType]; ok {
return idProvider.GetIDAndPayload(ctx, spaceID, sn, createdTime, getExisting, origin)
}
return "", treestorage.TreeStorageCreatePayload{}, fmt.Errorf("unsupported smartblock to import")
}
func (p *Provider) GetInternalKey(sbType sb.SmartBlockType) string {
if idProvider, ok := p.idProviderBySmartBlockType[sbType]; ok {
if internalKeyProvider, ok := idProvider.(InternalKeyProvider); ok {
return internalKeyProvider.GetInternalKey(sbType)
}
}
return ""
}

View file

@ -20,7 +20,7 @@ func newWidget(spaceService space.Service) *widget {
return &widget{spaceService: spaceService}
}
func (w widget) GetIDAndPayload(ctx context.Context, spaceID string, sn *common.Snapshot, _ time.Time, _ bool, _ objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error) {
func (w widget) GetIDAndPayload(ctx context.Context, spaceID string, _ *common.Snapshot, _ time.Time, _ bool, _ objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error) {
spc, err := w.spaceService.Get(ctx, spaceID)
if err != nil {
return "", treestorage.TreeStorageCreatePayload{}, fmt.Errorf("get space : %w", err)

View file

@ -20,7 +20,7 @@ func newWorkspace(spaceService space.Service) *workspace {
return &workspace{spaceService: spaceService}
}
func (w *workspace) GetIDAndPayload(ctx context.Context, spaceID string, sn *common.Snapshot, _ time.Time, _ bool, _ objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error) {
func (w *workspace) GetIDAndPayload(ctx context.Context, spaceID string, _ *common.Snapshot, _ time.Time, _ bool, _ objectorigin.ObjectOrigin) (string, treestorage.TreeStorageCreatePayload, error) {
spc, err := w.spaceService.Get(ctx, spaceID)
if err != nil {
return "", treestorage.TreeStorageCreatePayload{}, fmt.Errorf("get space : %w", err)

View file

@ -60,7 +60,7 @@ type Import struct {
converters map[string]common.Converter
s *block.Service
oc creator.Service
idProvider objectid.IDProvider
idProvider objectid.IdAndKeyProvider
tempDirProvider core.TempDirProvider
fileStore filestore.FileStore
fileSync filesync.FileSync
@ -431,6 +431,20 @@ func (i *Import) getObjectID(
if payload.RootRawChange != nil {
createPayloads[id] = payload
}
return i.extractInternalKey(snapshot, oldIDToNew)
}
func (i *Import) extractInternalKey(snapshot *common.Snapshot, oldIDToNew map[string]string) error {
newUniqueKey := i.idProvider.GetInternalKey(snapshot.SbType)
if newUniqueKey != "" {
oldUniqueKey := pbtypes.GetString(snapshot.Snapshot.Data.Details, bundle.RelationKeyUniqueKey.String())
if oldUniqueKey == "" {
oldUniqueKey = snapshot.Snapshot.Data.Key
}
if oldUniqueKey != "" {
oldIDToNew[oldUniqueKey] = newUniqueKey
}
}
return nil
}

View file

@ -6,16 +6,18 @@ import (
"fmt"
"testing"
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"go.uber.org/mock/gomock"
"github.com/anyproto/anytype-heart/core/block/import/common/objectid/mock_objectid"
"github.com/anyproto/anytype-heart/core/block/import/common"
"github.com/anyproto/anytype-heart/core/block/import/common/mock_common"
"github.com/anyproto/anytype-heart/core/block/import/common/objectcreator/mock_objectcreator"
"github.com/anyproto/anytype-heart/core/block/import/common/objectid/mock_objectid"
pbc "github.com/anyproto/anytype-heart/core/block/import/pb"
"github.com/anyproto/anytype-heart/core/block/import/web"
"github.com/anyproto/anytype-heart/core/block/import/web/parsers"
@ -54,8 +56,9 @@ func Test_ImportSuccess(t *testing.T) {
creator.EXPECT().Create(mock.Anything, mock.Anything).Return(nil, "", nil).Times(1)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter.EXPECT().GetID(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("").Times(1)
idGetter.EXPECT().GetIDAndPayload(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
fileSync := mock_filesync.NewMockFileSync(t)
@ -86,7 +89,7 @@ func Test_ImportErrorFromConverter(t *testing.T) {
i.converters["Notion"] = converter
creator := mock_objectcreator.NewMockService(t)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
i.idProvider = idGetter
fileSync := mock_filesync.NewMockFileSync(t)
@ -133,8 +136,9 @@ func Test_ImportErrorFromObjectCreator(t *testing.T) {
//nolint:lll
creator.EXPECT().Create(mock.Anything, mock.Anything).Return(nil, "", errors.New("creator error")).Times(1)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter.EXPECT().GetID(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetIDAndPayload(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("").Times(1)
i.idProvider = idGetter
fileSync := mock_filesync.NewMockFileSync(t)
@ -181,8 +185,9 @@ func Test_ImportIgnoreErrorMode(t *testing.T) {
creator := mock_objectcreator.NewMockService(t)
creator.EXPECT().Create(mock.Anything, mock.Anything).Return(nil, "", nil).Times(1)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter.EXPECT().GetID(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("").Times(1)
idGetter.EXPECT().GetIDAndPayload(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
fileSync := mock_filesync.NewMockFileSync(t)
@ -231,8 +236,9 @@ func Test_ImportIgnoreErrorModeWithTwoErrorsPerFile(t *testing.T) {
//nolint:lll
creator.EXPECT().Create(mock.Anything, mock.Anything).Return(nil, "", errors.New("creator error")).Times(1)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter.EXPECT().GetID(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("").Times(1)
idGetter.EXPECT().GetIDAndPayload(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
fileSync := mock_filesync.NewMockFileSync(t)
@ -259,8 +265,9 @@ func Test_ImportExternalPlugin(t *testing.T) {
creator := mock_objectcreator.NewMockService(t)
creator.EXPECT().Create(mock.Anything, mock.Anything).Return(nil, "", nil).Times(1)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter.EXPECT().GetID(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("").Times(1)
idGetter.EXPECT().GetIDAndPayload(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
fileSync := mock_filesync.NewMockFileSync(t)
@ -308,7 +315,7 @@ func Test_ImportExternalPluginError(t *testing.T) {
creator := mock_objectcreator.NewMockService(t)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
i.idProvider = idGetter
fileSync := mock_filesync.NewMockFileSync(t)
@ -334,7 +341,7 @@ func Test_ListImports(t *testing.T) {
i.converters["Notion"] = pbc.New(nil, nil, nil)
creator := mock_objectcreator.NewMockService(t)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
i.idProvider = idGetter
res, err := i.ListImports(&pb.RpcObjectImportListRequest{})
@ -351,7 +358,7 @@ func Test_ImportWebNoParser(t *testing.T) {
creator := mock_objectcreator.NewMockService(t)
i.oc = creator
i.idProvider = mock_objectid.NewMockIDGetter(t)
i.idProvider = mock_objectid.NewMockIdAndKeyProvider(t)
_, _, err := i.ImportWeb(context.Background(), &pb.RpcObjectImportRequest{
Params: &pb.RpcObjectImportRequestParamsOfBookmarksParams{BookmarksParams: &pb.RpcObjectImportRequestBookmarksParams{Url: "http://example.com"}},
UpdateExistingObjects: true,
@ -370,7 +377,7 @@ func Test_ImportWebFailedToParse(t *testing.T) {
i.converters[web.Name] = web.NewConverter()
creator := mock_objectcreator.NewMockService(t)
i.oc = creator
i.idProvider = mock_objectid.NewMockIDGetter(t)
i.idProvider = mock_objectid.NewMockIdAndKeyProvider(t)
parser := parsers.NewMockParser(ctrl)
parser.EXPECT().MatchUrl("http://example.com").Return(true).Times(1)
parser.EXPECT().ParseUrl("http://example.com").Return(nil, errors.New("failed")).Times(1)
@ -401,8 +408,9 @@ func Test_ImportWebSuccess(t *testing.T) {
creator := mock_objectcreator.NewMockService(t)
creator.EXPECT().Create(mock.Anything, mock.Anything).Return(nil, "", nil).Times(1)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter.EXPECT().GetID(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("").Times(1)
idGetter.EXPECT().GetIDAndPayload(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
parser := parsers.NewMockParser(ctrl)
parser.EXPECT().MatchUrl("http://example.com").Return(true).Times(1)
@ -442,8 +450,9 @@ func Test_ImportWebFailedToCreateObject(t *testing.T) {
//nolint:lll
creator.EXPECT().Create(mock.Anything, mock.Anything).Return(nil, "", errors.New("error")).Times(1)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter.EXPECT().GetID(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("").Times(1)
idGetter.EXPECT().GetIDAndPayload(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
parser := parsers.NewMockParser(ctrl)
parser.EXPECT().MatchUrl("http://example.com").Return(true).Times(1)
@ -585,8 +594,9 @@ func Test_ImportNoObjectToImportErrorIgnoreErrorsMode(t *testing.T) {
//nolint:lll
creator.EXPECT().Create(mock.Anything, mock.Anything).Return(nil, "", nil).Times(1)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter.EXPECT().GetID(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("").Times(1)
idGetter.EXPECT().GetIDAndPayload(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
fileSync := mock_filesync.NewMockFileSync(t)
@ -777,8 +787,9 @@ func Test_ImportRootCollectionInResponse(t *testing.T) {
creator.EXPECT().Create(mock.Anything, mock.Anything).Return(nil, "", nil).Times(1)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter.EXPECT().GetID(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedRootCollectionID, treestorage.TreeStorageCreatePayload{}, nil).Times(1)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("").Times(1)
idGetter.EXPECT().GetIDAndPayload(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(expectedRootCollectionID, treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
fileSync := mock_filesync.NewMockFileSync(t)
@ -825,8 +836,9 @@ func Test_ImportRootCollectionInResponse(t *testing.T) {
creator.EXPECT().Create(mock.Anything, mock.Anything).Return(nil, "", creatorError).Times(1)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter.EXPECT().GetID(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("").Times(1)
idGetter.EXPECT().GetIDAndPayload(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
fileSync := mock_filesync.NewMockFileSync(t)
@ -909,8 +921,9 @@ func Test_ImportRootCollectionInResponse(t *testing.T) {
creator.EXPECT().Create(mock.Anything, mock.Anything).Return(nil, "", nil).Times(1)
i.oc = creator
idGetter := mock_objectid.NewMockIDGetter(t)
idGetter.EXPECT().GetID(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("").Times(1)
idGetter.EXPECT().GetIDAndPayload(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return("id", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
fileSync := mock_filesync.NewMockFileSync(t)
@ -931,3 +944,114 @@ func Test_ImportRootCollectionInResponse(t *testing.T) {
assert.Equal(t, expectedRootCollectionId, res.RootCollectionId)
})
}
func Test_getObjectId(t *testing.T) {
t.Run("get object new id", func(t *testing.T) {
// given
i := Import{}
oldIDToNew := make(map[string]string, 0)
createPayloads := make(map[string]treestorage.TreeStorageCreatePayload, 0)
sn := &common.Snapshot{
Id: "oldId",
Snapshot: &pb.ChangeSnapshot{
Data: &model.SmartBlockSnapshotBase{},
},
}
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("").Times(1)
idGetter.EXPECT().GetIDAndPayload(context.Background(), "spaceId", sn, mock.Anything, false, objectorigin.Import(model.Import_Pb)).Return("newId", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
// when
err := i.getObjectID(context.Background(), "spaceId", sn, createPayloads, oldIDToNew, false, objectorigin.Import(model.Import_Pb))
// then
assert.Nil(t, err)
assert.Equal(t, "newId", oldIDToNew["oldId"])
})
t.Run("get object new id and new key", func(t *testing.T) {
// given
i := Import{}
oldIDToNew := make(map[string]string, 0)
createPayloads := make(map[string]treestorage.TreeStorageCreatePayload, 0)
sn := &common.Snapshot{
Id: "oldId",
Snapshot: &pb.ChangeSnapshot{
Data: &model.SmartBlockSnapshotBase{
Key: "key",
},
},
}
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("newKey").Times(1)
idGetter.EXPECT().GetIDAndPayload(context.Background(), "spaceId", sn, mock.Anything, false, objectorigin.Import(model.Import_Pb)).Return("newId", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
// when
err := i.getObjectID(context.Background(), "spaceId", sn, createPayloads, oldIDToNew, false, objectorigin.Import(model.Import_Pb))
// then
assert.Nil(t, err)
assert.Equal(t, "newId", oldIDToNew["oldId"])
assert.Equal(t, "newKey", oldIDToNew["key"])
})
t.Run("get object new id and new key", func(t *testing.T) {
// given
i := Import{}
oldIDToNew := make(map[string]string, 0)
createPayloads := make(map[string]treestorage.TreeStorageCreatePayload, 0)
sn := &common.Snapshot{
Id: "oldId",
Snapshot: &pb.ChangeSnapshot{
Data: &model.SmartBlockSnapshotBase{
Details: &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyUniqueKey.String(): pbtypes.String("key"),
}},
},
},
}
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("newKey").Times(1)
idGetter.EXPECT().GetIDAndPayload(context.Background(), "spaceId", sn, mock.Anything, false, objectorigin.Import(model.Import_Pb)).Return("newId", treestorage.TreeStorageCreatePayload{}, nil).Times(1)
i.idProvider = idGetter
// when
err := i.getObjectID(context.Background(), "spaceId", sn, createPayloads, oldIDToNew, false, objectorigin.Import(model.Import_Pb))
// then
assert.Nil(t, err)
assert.Equal(t, "newId", oldIDToNew["oldId"])
assert.Equal(t, "newKey", oldIDToNew["key"])
})
t.Run("don't add create payload", func(t *testing.T) {
// given
i := Import{}
oldIDToNew := make(map[string]string, 0)
createPayloads := make(map[string]treestorage.TreeStorageCreatePayload, 0)
sn := &common.Snapshot{
Id: "oldId",
Snapshot: &pb.ChangeSnapshot{
Data: &model.SmartBlockSnapshotBase{
Details: &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyUniqueKey.String(): pbtypes.String("key"),
}},
},
},
}
idGetter := mock_objectid.NewMockIdAndKeyProvider(t)
idGetter.EXPECT().GetInternalKey(mock.Anything).Return("newKey").Times(1)
idGetter.EXPECT().GetIDAndPayload(context.Background(), "spaceId", sn, mock.Anything, false, objectorigin.Import(model.Import_Pb)).Return("newId", treestorage.TreeStorageCreatePayload{
RootRawChange: &treechangeproto.RawTreeChangeWithId{},
}, nil).Times(1)
i.idProvider = idGetter
// when
err := i.getObjectID(context.Background(), "spaceId", sn, createPayloads, oldIDToNew, false, objectorigin.Import(model.Import_Pb))
// then
assert.Nil(t, err)
assert.Equal(t, "newId", oldIDToNew["oldId"])
assert.Equal(t, "newKey", oldIDToNew["key"])
assert.NotNil(t, createPayloads["newId"])
})
}

View file

@ -8,7 +8,6 @@ import (
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
"github.com/gogo/protobuf/types"
"github.com/samber/lo"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/block/editor/objecttype"
@ -165,15 +164,6 @@ func (s *service) reinstallBundledObjects(ctx context.Context, sourceSpace clien
return nil, nil, fmt.Errorf("query deleted objects: %w", err)
}
archivedObjects, err := s.queryArchivedObjects(space, sourceObjectIDs)
if err != nil {
log.Errorf("query archived objects: %w", err)
}
deletedObjects = lo.UniqBy(append(deletedObjects, archivedObjects...), func(record database.Record) string {
return pbtypes.GetString(record.Details, bundle.RelationKeyId.String())
})
var (
ids []string
objects []*types.Struct
@ -279,48 +269,32 @@ func (s *service) prepareDetailsForInstallingObject(
return details, nil
}
func (s *service) queryDeletedObjects(space clientspace.Space, sourceObjectIDs []string) (deletedObjects []database.Record, err error) {
deletedObjects, err = s.objectStore.Query(database.Query{
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeySourceObject.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.StringList(sourceObjectIDs),
func (s *service) queryDeletedObjects(space clientspace.Space, sourceObjectIDs []string) ([]database.Record, error) {
sourceList, err := pbtypes.ValueListWrapper(pbtypes.StringList(sourceObjectIDs))
if err != nil {
return nil, err
}
return s.objectStore.QueryRaw(&database.Filters{FilterObj: database.FiltersAnd{
database.FilterIn{
Key: bundle.RelationKeySourceObject.String(),
Value: sourceList,
},
database.FilterEq{
Key: bundle.RelationKeySpaceId.String(),
Cond: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(space.Id()),
},
database.FiltersOr{
database.FilterEq{
Key: bundle.RelationKeyIsDeleted.String(),
Cond: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Bool(true),
},
{
RelationKey: bundle.RelationKeySpaceId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(space.Id()),
},
{
RelationKey: bundle.RelationKeyIsDeleted.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Bool(true),
database.FilterEq{
Key: bundle.RelationKeyIsArchived.String(),
Cond: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Bool(true),
},
},
})
return
}
func (s *service) queryArchivedObjects(space clientspace.Space, sourceObjectIDs []string) (archivedObjects []database.Record, err error) {
archivedObjects, err = s.objectStore.Query(database.Query{
Filters: []*model.BlockContentDataviewFilter{
{
RelationKey: bundle.RelationKeySourceObject.String(),
Condition: model.BlockContentDataviewFilter_In,
Value: pbtypes.StringList(sourceObjectIDs),
},
{
RelationKey: bundle.RelationKeySpaceId.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.String(space.Id()),
},
{
RelationKey: bundle.RelationKeyIsArchived.String(),
Condition: model.BlockContentDataviewFilter_Equal,
Value: pbtypes.Bool(true),
},
},
})
return
}}, 0, 0)
}

View file

@ -32,9 +32,7 @@ func (s *service) createSet(ctx context.Context, space clientspace.Space, req *p
newState.AddDetails(req.Details)
newState.BlocksInit(newState)
tmpls := []template.StateTransformer{
template.WithRequiredRelations(),
}
tmpls := []template.StateTransformer{}
for i, view := range dvContent.Dataview.Views {
if view.Relations == nil {

View file

@ -98,7 +98,9 @@ func (s *service) CreateSmartBlockFromStateInSpaceWithOptions(
return "", nil, err
}
sb.Lock()
newDetails = sb.CombinedDetails()
sb.Unlock()
id = sb.Id()
if sbType == coresb.SmartBlockTypeObjectType && pbtypes.GetInt64(newDetails, bundle.RelationKeyLastUsedDate.String()) == 0 {

View file

@ -74,12 +74,13 @@ func (gr *Builder) ObjectGraph(req *pb.RpcObjectGraphRequest) ([]*types.Struct,
return rel.Key, isRelationShouldBeIncludedAsEdge(rel)
})...)
resp, err := gr.subscriptionService.Search(pb.RpcObjectSearchSubscribeRequest{
resp, err := gr.subscriptionService.Search(subscription.SubscribeRequest{
Source: req.SetSource,
Filters: req.Filters,
Keys: lo.Map(relations.Models(), func(rel *model.Relation, _ int) string { return rel.Key }),
CollectionId: req.CollectionId,
Limit: int64(req.Limit),
Internal: true,
})
if err != nil {

View file

@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/mock"
"github.com/anyproto/anytype-heart/core/relationutils"
"github.com/anyproto/anytype-heart/core/subscription"
"github.com/anyproto/anytype-heart/core/subscription/mock_subscription"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
@ -51,7 +52,7 @@ func Test(t *testing.T) {
{Relation: bundle.MustGetRelation(bundle.RelationKeyAuthor)},
{Relation: bundle.MustGetRelation(bundle.RelationKeyAttachments)},
}, nil)
fixture.subscriptionServiceMock.EXPECT().Search(mock.Anything).Return(&pb.RpcObjectSearchSubscribeResponse{
fixture.subscriptionServiceMock.EXPECT().Search(mock.Anything).Return(&subscription.SubscribeResponse{
Records: []*types.Struct{},
}, nil)
fixture.subscriptionServiceMock.EXPECT().Unsubscribe(mock.Anything).Return(nil)
@ -73,7 +74,7 @@ func Test(t *testing.T) {
{Relation: bundle.MustGetRelation(bundle.RelationKeyAssignee)},
{Relation: bundle.MustGetRelation(bundle.RelationKeyAttachments)},
}, nil)
fixture.subscriptionServiceMock.EXPECT().Search(mock.Anything).Return(&pb.RpcObjectSearchSubscribeResponse{
fixture.subscriptionServiceMock.EXPECT().Search(mock.Anything).Return(&subscription.SubscribeResponse{
Records: []*types.Struct{
{Fields: map[string]*types.Value{
bundle.RelationKeyId.String(): pbtypes.String("id1"),

View file

@ -4,7 +4,6 @@ package mock_treesyncer
import (
app "github.com/anyproto/any-sync/app"
domain "github.com/anyproto/anytype-heart/core/domain"
mock "github.com/stretchr/testify/mock"
)
@ -112,38 +111,37 @@ func (_c *MockSyncDetailsUpdater_Name_Call) RunAndReturn(run func() string) *Moc
return _c
}
// UpdateDetails provides a mock function with given fields: objectId, status, syncError, spaceId
func (_m *MockSyncDetailsUpdater) UpdateDetails(objectId []string, status domain.ObjectSyncStatus, syncError domain.SyncError, spaceId string) {
_m.Called(objectId, status, syncError, spaceId)
// UpdateSpaceDetails provides a mock function with given fields: existing, missing, spaceId
func (_m *MockSyncDetailsUpdater) UpdateSpaceDetails(existing []string, missing []string, spaceId string) {
_m.Called(existing, missing, spaceId)
}
// MockSyncDetailsUpdater_UpdateDetails_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateDetails'
type MockSyncDetailsUpdater_UpdateDetails_Call struct {
// MockSyncDetailsUpdater_UpdateSpaceDetails_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateSpaceDetails'
type MockSyncDetailsUpdater_UpdateSpaceDetails_Call struct {
*mock.Call
}
// UpdateDetails is a helper method to define mock.On call
// - objectId []string
// - status domain.ObjectSyncStatus
// - syncError domain.SyncError
// UpdateSpaceDetails is a helper method to define mock.On call
// - existing []string
// - missing []string
// - spaceId string
func (_e *MockSyncDetailsUpdater_Expecter) UpdateDetails(objectId interface{}, status interface{}, syncError interface{}, spaceId interface{}) *MockSyncDetailsUpdater_UpdateDetails_Call {
return &MockSyncDetailsUpdater_UpdateDetails_Call{Call: _e.mock.On("UpdateDetails", objectId, status, syncError, spaceId)}
func (_e *MockSyncDetailsUpdater_Expecter) UpdateSpaceDetails(existing interface{}, missing interface{}, spaceId interface{}) *MockSyncDetailsUpdater_UpdateSpaceDetails_Call {
return &MockSyncDetailsUpdater_UpdateSpaceDetails_Call{Call: _e.mock.On("UpdateSpaceDetails", existing, missing, spaceId)}
}
func (_c *MockSyncDetailsUpdater_UpdateDetails_Call) Run(run func(objectId []string, status domain.ObjectSyncStatus, syncError domain.SyncError, spaceId string)) *MockSyncDetailsUpdater_UpdateDetails_Call {
func (_c *MockSyncDetailsUpdater_UpdateSpaceDetails_Call) Run(run func(existing []string, missing []string, spaceId string)) *MockSyncDetailsUpdater_UpdateSpaceDetails_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]string), args[1].(domain.ObjectSyncStatus), args[2].(domain.SyncError), args[3].(string))
run(args[0].([]string), args[1].([]string), args[2].(string))
})
return _c
}
func (_c *MockSyncDetailsUpdater_UpdateDetails_Call) Return() *MockSyncDetailsUpdater_UpdateDetails_Call {
func (_c *MockSyncDetailsUpdater_UpdateSpaceDetails_Call) Return() *MockSyncDetailsUpdater_UpdateSpaceDetails_Call {
_c.Call.Return()
return _c
}
func (_c *MockSyncDetailsUpdater_UpdateDetails_Call) RunAndReturn(run func([]string, domain.ObjectSyncStatus, domain.SyncError, string)) *MockSyncDetailsUpdater_UpdateDetails_Call {
func (_c *MockSyncDetailsUpdater_UpdateSpaceDetails_Call) RunAndReturn(run func([]string, []string, string)) *MockSyncDetailsUpdater_UpdateSpaceDetails_Call {
_c.Call.Return(run)
return _c
}

View file

@ -15,8 +15,6 @@ import (
"github.com/anyproto/any-sync/net/streampool"
"github.com/anyproto/any-sync/nodeconf"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/domain"
)
var log = logger.NewNamed(treemanager.CName)
@ -62,14 +60,9 @@ type SyncedTreeRemover interface {
RemoveAllExcept(senderId string, differentRemoteIds []string)
}
type PeerStatusChecker interface {
app.Component
IsPeerOffline(peerId string) bool
}
type SyncDetailsUpdater interface {
app.Component
UpdateDetails(objectId []string, status domain.ObjectSyncStatus, syncError domain.SyncError, spaceId string)
UpdateSpaceDetails(existing, missing []string, spaceId string)
}
type treeSyncer struct {
@ -84,7 +77,6 @@ type treeSyncer struct {
treeManager treemanager.TreeManager
isRunning bool
isSyncing bool
peerManager PeerStatusChecker
nodeConf nodeconf.NodeConf
syncedTreeRemover SyncedTreeRemover
syncDetailsUpdater SyncDetailsUpdater
@ -106,7 +98,6 @@ func NewTreeSyncer(spaceId string) treesyncer.TreeSyncer {
func (t *treeSyncer) Init(a *app.App) (err error) {
t.isSyncing = true
t.treeManager = app.MustComponent[treemanager.TreeManager](a)
t.peerManager = app.MustComponent[PeerStatusChecker](a)
t.nodeConf = app.MustComponent[nodeconf.NodeConf](a)
t.syncedTreeRemover = app.MustComponent[SyncedTreeRemover](a)
t.syncDetailsUpdater = app.MustComponent[SyncDetailsUpdater](a)
@ -161,13 +152,11 @@ func (t *treeSyncer) ShouldSync(peerId string) bool {
return t.isSyncing
}
func (t *treeSyncer) SyncAll(ctx context.Context, peerId string, existing, missing []string) error {
func (t *treeSyncer) SyncAll(ctx context.Context, peerId string, existing, missing []string) (err error) {
t.Lock()
defer t.Unlock()
var err error
isResponsible := slices.Contains(t.nodeConf.NodeIds(t.spaceId), peerId)
defer t.sendResultEvent(err, isResponsible, peerId, existing)
t.sendSyncingEvent(peerId, existing, missing, isResponsible)
t.sendSyncEvents(existing, missing, isResponsible)
reqExec, exists := t.requestPools[peerId]
if !exists {
reqExec = newExecutor(t.requests, 0)
@ -206,31 +195,15 @@ func (t *treeSyncer) SyncAll(ctx context.Context, peerId string, existing, missi
return nil
}
func (t *treeSyncer) sendSyncingEvent(peerId string, existing []string, missing []string, nodePeer bool) {
func (t *treeSyncer) sendSyncEvents(existing, missing []string, nodePeer bool) {
if !nodePeer {
return
}
if t.peerManager.IsPeerOffline(peerId) {
t.sendDetailsUpdates(existing, domain.ObjectError, domain.NetworkError)
return
}
if len(existing) != 0 || len(missing) != 0 {
t.sendDetailsUpdates(existing, domain.ObjectSyncing, domain.Null)
}
t.sendDetailsUpdates(existing, missing)
}
func (t *treeSyncer) sendResultEvent(err error, nodePeer bool, peerId string, existing []string) {
if nodePeer && !t.peerManager.IsPeerOffline(peerId) {
if err != nil {
t.sendDetailsUpdates(existing, domain.ObjectError, domain.NetworkError)
} else {
t.sendDetailsUpdates(existing, domain.ObjectSynced, domain.Null)
}
}
}
func (t *treeSyncer) sendDetailsUpdates(existing []string, status domain.ObjectSyncStatus, syncError domain.SyncError) {
t.syncDetailsUpdater.UpdateDetails(existing, status, syncError, t.spaceId)
func (t *treeSyncer) sendDetailsUpdates(existing, missing []string) {
t.syncDetailsUpdater.UpdateSpaceDetails(existing, missing, t.spaceId)
}
func (t *treeSyncer) requestTree(peerId, id string) {
@ -257,6 +230,7 @@ func (t *treeSyncer) updateTree(peerId, id string) {
syncTree, ok := tr.(synctree.SyncTree)
if !ok {
log.Warn("not a sync tree")
return
}
if err = syncTree.SyncWithPeer(ctx, peerId); err != nil {
log.Warn("synctree.SyncWithPeer error", zap.Error(err))

View file

@ -16,7 +16,6 @@ import (
"go.uber.org/mock/gomock"
"github.com/anyproto/anytype-heart/core/block/object/treesyncer/mock_treesyncer"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/tests/testutil"
)
@ -26,7 +25,6 @@ type fixture struct {
missingMock *mock_objecttree.MockObjectTree
existingMock *mock_synctree.MockSyncTree
treeManager *mock_treemanager.MockTreeManager
checker *mock_treesyncer.MockPeerStatusChecker
nodeConf *mock_nodeconf.MockService
syncStatus *mock_treesyncer.MockSyncedTreeRemover
syncDetailsUpdater *mock_treesyncer.MockSyncDetailsUpdater
@ -37,8 +35,6 @@ func newFixture(t *testing.T, spaceId string) *fixture {
treeManager := mock_treemanager.NewMockTreeManager(ctrl)
missingMock := mock_objecttree.NewMockObjectTree(ctrl)
existingMock := mock_synctree.NewMockSyncTree(ctrl)
checker := mock_treesyncer.NewMockPeerStatusChecker(t)
checker.EXPECT().Name().Return("checker").Maybe()
nodeConf := mock_nodeconf.NewMockService(ctrl)
nodeConf.EXPECT().Name().Return("nodeConf").AnyTimes()
syncStatus := mock_treesyncer.NewMockSyncedTreeRemover(t)
@ -46,7 +42,6 @@ func newFixture(t *testing.T, spaceId string) *fixture {
a := new(app.App)
a.Register(testutil.PrepareMock(context.Background(), a, treeManager)).
Register(testutil.PrepareMock(context.Background(), a, checker)).
Register(testutil.PrepareMock(context.Background(), a, syncStatus)).
Register(testutil.PrepareMock(context.Background(), a, nodeConf)).
Register(testutil.PrepareMock(context.Background(), a, syncDetailsUpdater))
@ -59,7 +54,6 @@ func newFixture(t *testing.T, spaceId string) *fixture {
missingMock: missingMock,
existingMock: existingMock,
treeManager: treeManager,
checker: checker,
nodeConf: nodeConf,
syncStatus: syncStatus,
syncDetailsUpdater: syncDetailsUpdater,
@ -91,6 +85,25 @@ func TestTreeSyncer(t *testing.T) {
fx.Close(ctx)
})
t.Run("delayed sync notify sync status", func(t *testing.T) {
ctx := context.Background()
fx := newFixture(t, spaceId)
fx.treeManager.EXPECT().GetTree(gomock.Any(), spaceId, existingId).Return(fx.existingMock, nil)
fx.existingMock.EXPECT().SyncWithPeer(gomock.Any(), peerId).Return(nil)
fx.treeManager.EXPECT().GetTree(gomock.Any(), spaceId, missingId).Return(fx.missingMock, nil)
fx.nodeConf.EXPECT().NodeIds(spaceId).Return([]string{peerId})
fx.syncDetailsUpdater.EXPECT().UpdateSpaceDetails([]string{existingId}, []string{missingId}, spaceId)
fx.syncStatus.EXPECT().RemoveAllExcept(peerId, []string{existingId}).Return()
err := fx.SyncAll(context.Background(), peerId, []string{existingId}, []string{missingId})
require.NoError(t, err)
require.NotNil(t, fx.requestPools[peerId])
require.NotNil(t, fx.headPools[peerId])
fx.StartSync()
time.Sleep(100 * time.Millisecond)
fx.Close(ctx)
})
t.Run("sync after run", func(t *testing.T) {
ctx := context.Background()
fx := newFixture(t, spaceId)
@ -189,45 +202,5 @@ func TestTreeSyncer(t *testing.T) {
require.Equal(t, []string{"before close", "after done"}, events)
mutex.Unlock()
})
t.Run("send offline event", func(t *testing.T) {
ctx := context.Background()
fx := newFixture(t, spaceId)
fx.treeManager.EXPECT().GetTree(gomock.Any(), spaceId, existingId).Return(fx.existingMock, nil)
fx.existingMock.EXPECT().SyncWithPeer(gomock.Any(), peerId).Return(nil)
fx.treeManager.EXPECT().GetTree(gomock.Any(), spaceId, missingId).Return(fx.missingMock, nil)
fx.nodeConf.EXPECT().NodeIds(spaceId).Return([]string{peerId})
fx.checker.EXPECT().IsPeerOffline(peerId).Return(true)
fx.syncStatus.EXPECT().RemoveAllExcept(peerId, []string{existingId}).Return()
fx.syncDetailsUpdater.EXPECT().UpdateDetails([]string{"existing"}, domain.ObjectError, domain.NetworkError, "spaceId").Return()
fx.StartSync()
err := fx.SyncAll(context.Background(), peerId, []string{existingId}, []string{missingId})
require.NoError(t, err)
require.NotNil(t, fx.requestPools[peerId])
require.NotNil(t, fx.headPools[peerId])
time.Sleep(100 * time.Millisecond)
fx.Close(ctx)
})
t.Run("send syncing and synced event", func(t *testing.T) {
ctx := context.Background()
fx := newFixture(t, spaceId)
fx.treeManager.EXPECT().GetTree(gomock.Any(), spaceId, existingId).Return(fx.existingMock, nil)
fx.existingMock.EXPECT().SyncWithPeer(gomock.Any(), peerId).Return(nil)
fx.treeManager.EXPECT().GetTree(gomock.Any(), spaceId, missingId).Return(fx.missingMock, nil)
fx.nodeConf.EXPECT().NodeIds(spaceId).Return([]string{peerId})
fx.checker.EXPECT().IsPeerOffline(peerId).Return(false)
fx.syncStatus.EXPECT().RemoveAllExcept(peerId, []string{existingId}).Return()
fx.syncDetailsUpdater.EXPECT().UpdateDetails([]string{"existing"}, domain.ObjectSynced, domain.Null, "spaceId").Return()
fx.syncDetailsUpdater.EXPECT().UpdateDetails([]string{"existing"}, domain.ObjectSyncing, domain.Null, "spaceId").Return()
fx.StartSync()
err := fx.SyncAll(context.Background(), peerId, []string{existingId}, []string{missingId})
require.NoError(t, err)
require.NotNil(t, fx.requestPools[peerId])
require.NotNil(t, fx.headPools[peerId])
time.Sleep(100 * time.Millisecond)
fx.Close(ctx)
})
}

View file

@ -124,6 +124,7 @@ type builtinObjects interface {
type templateService interface {
CreateTemplateStateWithDetails(templateId string, details *types.Struct) (*state.State, error)
CreateTemplateStateFromSmartBlock(sb smartblock.SmartBlock, details *types.Struct) *state.State
}
type openedObjects struct {

View file

@ -87,9 +87,9 @@ func (_c *MockService_DetailsFromIdBasedSource_Call) RunAndReturn(run func(strin
return _c
}
// IDsListerBySmartblockType provides a mock function with given fields: spaceID, blockType
func (_m *MockService) IDsListerBySmartblockType(spaceID string, blockType smartblock.SmartBlockType) (source.IDsLister, error) {
ret := _m.Called(spaceID, blockType)
// IDsListerBySmartblockType provides a mock function with given fields: space, blockType
func (_m *MockService) IDsListerBySmartblockType(space source.Space, blockType smartblock.SmartBlockType) (source.IDsLister, error) {
ret := _m.Called(space, blockType)
if len(ret) == 0 {
panic("no return value specified for IDsListerBySmartblockType")
@ -97,19 +97,19 @@ func (_m *MockService) IDsListerBySmartblockType(spaceID string, blockType smart
var r0 source.IDsLister
var r1 error
if rf, ok := ret.Get(0).(func(string, smartblock.SmartBlockType) (source.IDsLister, error)); ok {
return rf(spaceID, blockType)
if rf, ok := ret.Get(0).(func(source.Space, smartblock.SmartBlockType) (source.IDsLister, error)); ok {
return rf(space, blockType)
}
if rf, ok := ret.Get(0).(func(string, smartblock.SmartBlockType) source.IDsLister); ok {
r0 = rf(spaceID, blockType)
if rf, ok := ret.Get(0).(func(source.Space, smartblock.SmartBlockType) source.IDsLister); ok {
r0 = rf(space, blockType)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(source.IDsLister)
}
}
if rf, ok := ret.Get(1).(func(string, smartblock.SmartBlockType) error); ok {
r1 = rf(spaceID, blockType)
if rf, ok := ret.Get(1).(func(source.Space, smartblock.SmartBlockType) error); ok {
r1 = rf(space, blockType)
} else {
r1 = ret.Error(1)
}
@ -123,15 +123,15 @@ type MockService_IDsListerBySmartblockType_Call struct {
}
// IDsListerBySmartblockType is a helper method to define mock.On call
// - spaceID string
// - space source.Space
// - blockType smartblock.SmartBlockType
func (_e *MockService_Expecter) IDsListerBySmartblockType(spaceID interface{}, blockType interface{}) *MockService_IDsListerBySmartblockType_Call {
return &MockService_IDsListerBySmartblockType_Call{Call: _e.mock.On("IDsListerBySmartblockType", spaceID, blockType)}
func (_e *MockService_Expecter) IDsListerBySmartblockType(space interface{}, blockType interface{}) *MockService_IDsListerBySmartblockType_Call {
return &MockService_IDsListerBySmartblockType_Call{Call: _e.mock.On("IDsListerBySmartblockType", space, blockType)}
}
func (_c *MockService_IDsListerBySmartblockType_Call) Run(run func(spaceID string, blockType smartblock.SmartBlockType)) *MockService_IDsListerBySmartblockType_Call {
func (_c *MockService_IDsListerBySmartblockType_Call) Run(run func(space source.Space, blockType smartblock.SmartBlockType)) *MockService_IDsListerBySmartblockType_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string), args[1].(smartblock.SmartBlockType))
run(args[0].(source.Space), args[1].(smartblock.SmartBlockType))
})
return _c
}
@ -141,7 +141,7 @@ func (_c *MockService_IDsListerBySmartblockType_Call) Return(_a0 source.IDsListe
return _c
}
func (_c *MockService_IDsListerBySmartblockType_Call) RunAndReturn(run func(string, smartblock.SmartBlockType) (source.IDsLister, error)) *MockService_IDsListerBySmartblockType_Call {
func (_c *MockService_IDsListerBySmartblockType_Call) RunAndReturn(run func(source.Space, smartblock.SmartBlockType) (source.IDsLister, error)) *MockService_IDsListerBySmartblockType_Call {
_c.Call.Return(run)
return _c
}

View file

@ -23,7 +23,6 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
"github.com/anyproto/anytype-heart/space/spacecore"
"github.com/anyproto/anytype-heart/space/spacecore/storage"
"github.com/anyproto/anytype-heart/space/spacecore/typeprovider"
)
@ -41,10 +40,12 @@ type accountService interface {
type Space interface {
Id() string
IsPersonal() bool
TreeBuilder() objecttreebuilder.TreeBuilder
GetRelationIdByKey(ctx context.Context, key domain.RelationKey) (id string, err error)
GetTypeIdByKey(ctx context.Context, key domain.TypeKey) (id string, err error)
DeriveObjectID(ctx context.Context, uniqueKey domain.UniqueKey) (id string, err error)
StoredIds() []string
}
type Service interface {
@ -53,7 +54,7 @@ type Service interface {
NewStaticSource(params StaticSourceParams) SourceWithType
DetailsFromIdBasedSource(id string) (*types.Struct, error)
IDsListerBySmartblockType(spaceID string, blockType smartblock.SmartBlockType) (IDsLister, error)
IDsListerBySmartblockType(space Space, blockType smartblock.SmartBlockType) (IDsLister, error)
app.Component
}
@ -61,7 +62,6 @@ type service struct {
sbtProvider typeprovider.SmartBlockTypeProvider
accountService accountService
accountKeysService accountservice.Service
spaceCoreService spacecore.SpaceCoreService
storageService storage.ClientStorage
fileService files.Service
objectStore RelationGetter
@ -77,7 +77,6 @@ func (s *service) Init(a *app.App) (err error) {
s.sbtProvider = a.MustComponent(typeprovider.CName).(typeprovider.SmartBlockTypeProvider)
s.accountService = app.MustComponent[accountService](a)
s.accountKeysService = a.MustComponent(accountservice.CName).(accountservice.Service)
s.spaceCoreService = app.MustComponent[spacecore.SpaceCoreService](a)
s.storageService = a.MustComponent(spacestorage.CName).(storage.ClientStorage)
s.fileService = app.MustComponent[files.Service](a)
@ -160,7 +159,7 @@ func (s *service) newSource(ctx context.Context, space Space, id string, buildOp
return s.newTreeSource(ctx, space, id, buildOptions.BuildTreeOpts())
}
func (s *service) IDsListerBySmartblockType(spaceID string, blockType smartblock.SmartBlockType) (IDsLister, error) {
func (s *service) IDsListerBySmartblockType(space Space, blockType smartblock.SmartBlockType) (IDsLister, error) {
switch blockType {
case smartblock.SmartBlockTypeAnytypeProfile:
return &anytypeProfile{}, nil
@ -181,9 +180,9 @@ func (s *service) IDsListerBySmartblockType(spaceID string, blockType smartblock
return nil, err
}
return &source{
spaceID: spaceID,
space: space,
spaceID: space.Id(),
smartblockType: blockType,
spaceService: s.spaceCoreService,
sbtProvider: s.sbtProvider,
}, nil
}

View file

@ -29,7 +29,6 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/space/spacecore"
"github.com/anyproto/anytype-heart/space/spacecore/typeprovider"
"github.com/anyproto/anytype-heart/util/slice"
)
@ -152,7 +151,6 @@ func (s *service) newTreeSource(ctx context.Context, space Space, id string, bui
id: id,
space: space,
spaceID: space.Id(),
spaceService: s.spaceCoreService,
smartblockType: sbt,
accountService: s.accountService,
accountKeysService: s.accountKeysService,
@ -173,7 +171,7 @@ type fileObjectMigrator interface {
}
type RelationGetter interface {
GetRelationByKey(key string) (*model.Relation, error)
GetRelationByKey(spaceId string, key string) (*model.Relation, error)
}
type source struct {
@ -191,7 +189,6 @@ type source struct {
fileService files.Service
accountService accountService
accountKeysService accountservice.Service
spaceService spacecore.SpaceCoreService
sbtProvider typeprovider.SmartBlockTypeProvider
objectStore RelationGetter
fileObjectMigrator fileObjectMigrator
@ -297,6 +294,8 @@ func (s *source) buildState() (doc state.Doc, err error) {
migration := NewSubObjectsAndProfileLinksMigration(s.smartblockType, s.space, s.accountService.MyParticipantId(s.spaceID), s.objectStore)
migration.Migrate(st)
// we need to have required internal relations for all objects, including system
st.AddBundledRelationLinks(bundle.RequiredInternalRelations...)
if s.Type() == smartblock.SmartBlockTypePage || s.Type() == smartblock.SmartBlockTypeProfilePage {
template.WithAddedFeaturedRelation(bundle.RelationKeyBacklinks)(st)
template.WithRelations([]domain.RelationKey{bundle.RelationKeyBacklinks})(st)
@ -350,7 +349,6 @@ func (s *source) PushChange(params PushChangeParams) (id string, err error) {
change := s.buildChange(params)
data, dataType, err := MarshalChange(change)
if err != nil {
return
}
@ -418,11 +416,10 @@ func checkChangeSize(data []byte, maxSize int) error {
}
func (s *source) ListIds() (ids []string, err error) {
spc, err := s.spaceService.Get(context.Background(), s.spaceID)
if err != nil {
if s.space == nil {
return
}
ids = slice.Filter(spc.StoredIds(), func(id string) bool {
ids = slice.Filter(s.space.StoredIds(), func(id string) bool {
t, err := s.sbtProvider.Type(s.spaceID, id)
if err != nil {
return false

View file

@ -91,7 +91,12 @@ func (m *subObjectsAndProfileLinksMigration) replaceLinksInDetails(s *state.Stat
}
}
// Migrate works only in personal space
func (m *subObjectsAndProfileLinksMigration) Migrate(s *state.State) {
if !m.space.IsPersonal() {
return
}
uk, err := domain.NewUniqueKey(smartblock.SmartBlockTypeProfilePage, "")
if err != nil {
log.Errorf("migration: failed to create unique key for profile: %s", err)
@ -187,7 +192,7 @@ func (m *subObjectsAndProfileLinksMigration) migrateFilter(filter *model.BlockCo
log.With("relationKey", filter.RelationKey).Warnf("empty filter value")
return nil
}
relation, err := m.objectStore.GetRelationByKey(filter.RelationKey)
relation, err := m.objectStore.GetRelationByKey(m.space.Id(), filter.RelationKey)
if err != nil {
log.Warnf("migration: failed to get relation by key %s: %s", filter.RelationKey, err)
}

View file

@ -51,6 +51,7 @@ var (
type Service interface {
CreateTemplateStateWithDetails(templateId string, details *types.Struct) (st *state.State, err error)
CreateTemplateStateFromSmartBlock(sb smartblock.SmartBlock, details *types.Struct) *state.State
ObjectApplyTemplate(contextId string, templateId string) error
TemplateCreateFromObject(ctx context.Context, id string) (templateId string, err error)
@ -106,13 +107,23 @@ func (s *service) CreateTemplateStateWithDetails(
return
}
}
targetDetails := extractTargetDetails(details, targetState.Details())
targetState.AddDetails(targetDetails)
targetState.BlocksInit(targetState)
addDetailsToState(targetState, details)
return targetState, nil
}
// CreateTemplateStateFromSmartBlock duplicates the logic of CreateTemplateStateWithDetails but does not take the lock on smartBlock.
// if building of state fails, state of blank template is returned
func (s *service) CreateTemplateStateFromSmartBlock(sb smartblock.SmartBlock, details *types.Struct) *state.State {
st, err := s.buildState(sb)
if err != nil {
layout := pbtypes.GetInt64(details, bundle.RelationKeyLayout.String())
st = s.createBlankTemplateState(model.ObjectTypeLayout(layout))
}
addDetailsToState(st, details)
return st
}
func extractTargetDetails(originDetails *types.Struct, templateDetails *types.Struct) *types.Struct {
targetDetails := pbtypes.CopyStruct(originDetails, true)
if templateDetails == nil {
@ -138,23 +149,7 @@ func extractTargetDetails(originDetails *types.Struct, templateDetails *types.St
func (s *service) createCustomTemplateState(templateId string) (targetState *state.State, err error) {
err = cache.Do(s.picker, templateId, func(sb smartblock.SmartBlock) (innerErr error) {
if !lo.Contains(sb.ObjectTypeKeys(), bundle.TypeKeyTemplate) {
return fmt.Errorf("object '%s' is not a template", templateId)
}
targetState = sb.NewState().Copy()
if pbtypes.GetBool(targetState.LocalDetails(), bundle.RelationKeyIsArchived.String()) {
return spacestorage.ErrTreeStorageAlreadyDeleted
}
innerErr = s.updateTypeKey(targetState)
if innerErr != nil {
return
}
targetState.RemoveDetail(bundle.RelationKeyTargetObjectType.String(), bundle.RelationKeyTemplateIsBundled.String(), bundle.RelationKeyOrigin.String())
targetState.SetDetailAndBundledRelation(bundle.RelationKeySourceObject, pbtypes.String(sb.Id()))
targetState.SetLocalDetails(nil)
targetState, innerErr = s.buildState(sb)
return
})
if errors.Is(err, spacestorage.ErrTreeStorageAlreadyDeleted) {
@ -163,6 +158,37 @@ func (s *service) createCustomTemplateState(templateId string) (targetState *sta
return
}
func (s *service) buildState(sb smartblock.SmartBlock) (st *state.State, err error) {
if sb == nil {
return nil, fmt.Errorf("smartblock is nil")
}
if !lo.Contains(sb.ObjectTypeKeys(), bundle.TypeKeyTemplate) {
return nil, fmt.Errorf("object '%s' is not a template", sb.Id())
}
st = sb.NewState().Copy()
if pbtypes.GetBool(st.LocalDetails(), bundle.RelationKeyIsArchived.String()) {
return nil, spacestorage.ErrTreeStorageAlreadyDeleted
}
err = s.updateTypeKey(st)
if err != nil {
return
}
st.RemoveDetail(
bundle.RelationKeyTargetObjectType.String(),
bundle.RelationKeyTemplateIsBundled.String(),
bundle.RelationKeyOrigin.String(),
bundle.RelationKeyAddedDate.String(),
)
st.SetDetailAndBundledRelation(bundle.RelationKeySourceObject, pbtypes.String(sb.Id()))
// original created timestamp is used to set creationDate for imported objects, not for template-based objects
st.SetOriginalCreatedTimestamp(0)
st.SetLocalDetails(nil)
return
}
func (s *service) ObjectApplyTemplate(contextId, templateId string) error {
return cache.Do(s.picker, contextId, func(b smartblock.SmartBlock) error {
orig := b.NewState().ParentState()
@ -306,7 +332,7 @@ func (s *service) createBlankTemplateState(layout model.ObjectTypeLayout) (st *s
template.WithDefaultFeaturedRelations,
template.WithFeaturedRelations,
template.WithAddedFeaturedRelation(bundle.RelationKeyTag),
template.WithRequiredRelations(),
template.WithDetail(bundle.RelationKeyTag, pbtypes.StringList(nil)),
template.WithTitle,
)
_ = s.converter.Convert(nil, st, model.ObjectType_basic, layout)
@ -356,3 +382,9 @@ func buildTemplateStateFromObject(sb smartblock.SmartBlock) (*state.State, error
flags.AddToState(st)
return st, nil
}
func addDetailsToState(s *state.State, details *types.Struct) {
targetDetails := extractTargetDetails(details, s.Details())
s.AddDetails(targetDetails)
s.BlocksInit(s)
}

View file

@ -5,12 +5,14 @@ import (
"fmt"
"strconv"
"testing"
"time"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/commonspace/spacestorage"
"github.com/gogo/protobuf/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/anyproto/anytype-heart/core/block/editor/converter"
@ -167,6 +169,7 @@ func TestService_CreateTemplateStateWithDetails(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, BlankTemplateId, st.RootId())
assert.Contains(t, pbtypes.GetStringList(st.Details(), bundle.RelationKeyFeaturedRelations.String()), bundle.RelationKeyTag.String())
assert.True(t, pbtypes.Exists(st.Details(), bundle.RelationKeyTag.String()))
})
}
@ -219,6 +222,58 @@ func TestService_CreateTemplateStateWithDetails(t *testing.T) {
assertLayoutBlocks(t, st, layout)
})
}
t.Run("do not inherit addedDate and creationDate", func(t *testing.T) {
// given
sometime := time.Now().Unix()
tmpl := smarttest.New(templateName)
tmpl.Doc.(*state.State).SetObjectTypeKeys([]domain.TypeKey{bundle.TypeKeyTemplate, bundle.TypeKeyBook})
tmpl.Doc.(*state.State).SetOriginalCreatedTimestamp(sometime)
err := tmpl.SetDetails(nil, []*model.Detail{{Key: bundle.RelationKeyAddedDate.String(), Value: pbtypes.Int64(sometime)}}, false)
require.NoError(t, err)
s := service{picker: &testPicker{tmpl}}
// when
st, err := s.CreateTemplateStateWithDetails(templateName, nil)
// then
assert.NoError(t, err)
assert.Zero(t, st.OriginalCreatedTimestamp())
assert.Zero(t, pbtypes.GetInt64(st.Details(), bundle.RelationKeyAddedDate.String()))
assert.Zero(t, pbtypes.GetInt64(st.Details(), bundle.RelationKeyCreatedDate.String()))
})
}
func TestCreateTemplateStateFromSmartBlock(t *testing.T) {
t.Run("if failed to build state -> return blank template", func(t *testing.T) {
// given
s := service{converter: converter.NewLayoutConverter()}
// when
st := s.CreateTemplateStateFromSmartBlock(nil, &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyLayout.String(): pbtypes.Int64(int64(model.ObjectType_todo)),
}})
// then
assert.Equal(t, BlankTemplateId, st.RootId())
assert.Contains(t, pbtypes.GetStringList(st.Details(), bundle.RelationKeyFeaturedRelations.String()), bundle.RelationKeyTag.String())
assert.True(t, pbtypes.Exists(st.Details(), bundle.RelationKeyTag.String()))
})
t.Run("create state from template smartblock", func(t *testing.T) {
// given
tmpl := NewTemplateTest("template", bundle.TypeKeyProject.String())
s := service{}
// when
st := s.CreateTemplateStateFromSmartBlock(tmpl, nil)
// then
assert.Equal(t, "template", pbtypes.GetString(st.Details(), bundle.RelationKeyName.String()))
assert.Equal(t, "template", pbtypes.GetString(st.Details(), bundle.RelationKeyDescription.String()))
})
}
func assertLayoutBlocks(t *testing.T, st *state.State, layout model.ObjectTypeLayout) {

22
core/device.go Normal file
View file

@ -0,0 +1,22 @@
package core
import (
"context"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/anytype-heart/core/device"
"github.com/anyproto/anytype-heart/pb"
)
func (mw *Middleware) DeviceNetworkStateSet(cctx context.Context, req *pb.RpcDeviceNetworkStateSetRequest) *pb.RpcDeviceNetworkStateSetResponse {
response := func(code pb.RpcDeviceNetworkStateSetResponseErrorCode, err error) *pb.RpcDeviceNetworkStateSetResponse {
m := &pb.RpcDeviceNetworkStateSetResponse{Error: &pb.RpcDeviceNetworkStateSetResponseError{Code: code}}
if err != nil {
m.Error.Description = err.Error()
}
return m
}
app.MustComponent[device.NetworkState](mw.GetApp()).SetNetworkState(req.DeviceNetworkType)
return response(pb.RpcDeviceNetworkStateSetResponseError_NULL, nil)
}

View file

@ -0,0 +1,67 @@
package device
import (
"sync"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
const CName = "networkState"
type OnNetworkUpdateHook func(network model.DeviceNetworkType)
type NetworkState interface {
app.Component
GetNetworkState() model.DeviceNetworkType
SetNetworkState(networkState model.DeviceNetworkType)
RegisterHook(hook OnNetworkUpdateHook)
}
type networkState struct {
networkState model.DeviceNetworkType
networkMu sync.Mutex
onNetworkUpdateHooks []OnNetworkUpdateHook
hookMu sync.Mutex
}
func New() NetworkState {
return &networkState{}
}
func (n *networkState) Init(a *app.App) (err error) {
return
}
func (n *networkState) Name() (name string) {
return CName
}
func (n *networkState) GetNetworkState() model.DeviceNetworkType {
n.networkMu.Lock()
defer n.networkMu.Unlock()
return n.networkState
}
func (n *networkState) SetNetworkState(networkState model.DeviceNetworkType) {
n.networkMu.Lock()
n.networkState = networkState
defer n.networkMu.Unlock()
n.runOnNetworkUpdateHook()
}
func (n *networkState) RegisterHook(hook OnNetworkUpdateHook) {
n.hookMu.Lock()
defer n.hookMu.Unlock()
n.onNetworkUpdateHooks = append(n.onNetworkUpdateHooks, hook)
}
func (n *networkState) runOnNetworkUpdateHook() {
n.hookMu.Lock()
defer n.hookMu.Unlock()
for _, hook := range n.onNetworkUpdateHooks {
hook(n.networkState)
}
}

View file

@ -0,0 +1,74 @@
package device
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
func TestNetworkState_SetNetworkState(t *testing.T) {
t.Run("set network state", func(t *testing.T) {
// given
state := &networkState{}
// when
state.SetNetworkState(model.DeviceNetworkType_CELLULAR)
// then
assert.Equal(t, model.DeviceNetworkType_CELLULAR, state.networkState)
})
t.Run("update network state", func(t *testing.T) {
// given
state := &networkState{}
// when
state.SetNetworkState(model.DeviceNetworkType_CELLULAR)
state.SetNetworkState(model.DeviceNetworkType_WIFI)
// then
assert.Equal(t, model.DeviceNetworkType_WIFI, state.networkState)
})
t.Run("update network state with hook", func(t *testing.T) {
// given
state := &networkState{}
var hookState model.DeviceNetworkType
h := func(state model.DeviceNetworkType) {
hookState = state
}
state.RegisterHook(h)
// when
state.SetNetworkState(model.DeviceNetworkType_CELLULAR)
state.SetNetworkState(model.DeviceNetworkType_WIFI)
// then
assert.Equal(t, model.DeviceNetworkType_WIFI, state.networkState)
assert.Equal(t, model.DeviceNetworkType_WIFI, hookState)
})
}
func TestNetworkState_GetNetworkState(t *testing.T) {
t.Run("get default network state", func(t *testing.T) {
// given
state := New()
// when
networkType := state.GetNetworkState()
// then
assert.Equal(t, model.DeviceNetworkType_WIFI, networkType)
})
t.Run("get updated network state", func(t *testing.T) {
// given
state := New()
// when
state.SetNetworkState(model.DeviceNetworkType_CELLULAR)
networkType := state.GetNetworkState()
// then
assert.Equal(t, model.DeviceNetworkType_CELLULAR, networkType)
})
}

177
core/device/service.go Normal file
View file

@ -0,0 +1,177 @@
package device
import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
"github.com/anyproto/any-sync/net/peer"
"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/object/objectcache"
"github.com/anyproto/anytype-heart/core/domain"
"github.com/anyproto/anytype-heart/core/wallet"
sb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
"github.com/anyproto/anytype-heart/pkg/lib/datastore"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/space"
"github.com/anyproto/anytype-heart/space/spacecore/peermanager"
)
const deviceService = "deviceService"
var log = logging.Logger(deviceService)
type Service interface {
app.ComponentRunnable
UpdateName(ctx context.Context, id, name string) error
ListDevices(ctx context.Context) ([]*model.DeviceInfo, error)
SaveDeviceInfo(info smartblock.ApplyInfo) error
}
func NewDevices() Service {
return &devices{finishLoad: make(chan struct{})}
}
type devices struct {
deviceObjectId string
spaceService space.Service
wallet wallet.Wallet
cancel context.CancelFunc
store Store
finishLoad chan struct{}
}
func (d *devices) Init(a *app.App) (err error) {
d.spaceService = app.MustComponent[space.Service](a)
d.wallet = a.MustComponent(wallet.CName).(wallet.Wallet)
datastoreService := app.MustComponent[datastore.Datastore](a)
db, err := datastoreService.LocalStorage()
if err != nil {
return fmt.Errorf("failed to initialize notification store %w", err)
}
d.store = NewStore(db)
return nil
}
func (d *devices) Name() (name string) {
return deviceService
}
func (d *devices) Run(ctx context.Context) error {
ctx, cancel := context.WithCancel(context.Background())
d.cancel = cancel
go d.loadDevices(ctx)
return nil
}
func (d *devices) loadDevices(ctx context.Context) {
defer close(d.finishLoad)
uk, err := domain.NewUniqueKey(sb.SmartBlockTypeDevicesObject, "")
if err != nil {
log.Errorf("failed to get devices object unique key: %v", err)
return
}
techSpace, err := d.spaceService.GetTechSpace(ctx)
if err != nil {
return
}
objectId, err := techSpace.DeriveObjectID(ctx, uk)
if err != nil {
log.Errorf("failed to derive device object id: %v", err)
return
}
d.deviceObjectId = objectId
ctx = context.WithValue(ctx, peermanager.ContextPeerFindDeadlineKey, time.Now().Add(30*time.Second))
ctx = peer.CtxWithPeerId(ctx, peer.CtxResponsiblePeers)
deviceObject, err := techSpace.GetObject(ctx, objectId)
if err != nil {
deviceObject, err = techSpace.DeriveTreeObject(ctx, objectcache.TreeDerivationParams{
Key: uk,
InitFunc: func(id string) *smartblock.InitContext {
return &smartblock.InitContext{
Ctx: ctx,
SpaceID: techSpace.Id(),
State: state.NewDoc(id, nil).(*state.State),
}
},
})
if err != nil && !errors.Is(err, treestorage.ErrTreeExists) {
log.Errorf("failed to derive device object: %v", err)
return
}
if err == nil {
d.deviceObjectId = deviceObject.Id()
}
}
hostname, err := os.Hostname()
if err != nil {
log.Errorf("failed to get hostname: %v", err)
return
}
deviceObject.Lock()
st := deviceObject.NewState()
deviceId := d.wallet.GetDevicePrivkey().GetPublic().PeerId()
st.AddDevice(&model.DeviceInfo{
Id: deviceId,
Name: hostname,
AddDate: time.Now().Unix(),
})
err = deviceObject.Apply(st)
if err != nil {
log.Errorf("failed to apply device state: %v", err)
}
deviceObject.Unlock()
}
func (d *devices) Close(ctx context.Context) error {
if d.cancel != nil {
d.cancel()
}
return nil
}
func (d *devices) SaveDeviceInfo(info smartblock.ApplyInfo) error {
if info.State == nil {
return nil
}
deviceId := d.wallet.GetDevicePrivkey().GetPublic().PeerId()
for _, deviceInfo := range info.State.ListDevices() {
if deviceInfo.Id == deviceId {
deviceInfo.IsConnected = true
}
err := d.store.SaveDevice(deviceInfo)
if err != nil {
return fmt.Errorf("failed to save device: %w", err)
}
}
return nil
}
func (d *devices) UpdateName(ctx context.Context, id, name string) error {
err := d.store.UpdateDeviceName(id, name)
if err != nil {
return fmt.Errorf("failed to update device name: %w", err)
}
spc, err := d.spaceService.Get(ctx, d.spaceService.TechSpaceId())
if err != nil {
return fmt.Errorf("failed to get space: %w", err)
}
return spc.Do(d.deviceObjectId, func(sb smartblock.SmartBlock) error {
st := sb.NewState()
st.SetDeviceName(id, name)
return sb.Apply(st)
})
}
func (d *devices) ListDevices(ctx context.Context) ([]*model.DeviceInfo, error) {
return d.store.ListDevices()
}

351
core/device/service_test.go Normal file
View file

@ -0,0 +1,351 @@
package device
import (
"context"
"fmt"
"os"
"testing"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/anyproto/anytype-heart/core/block/editor"
"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/block/object/objectcache/mock_objectcache"
wallet2 "github.com/anyproto/anytype-heart/core/wallet"
"github.com/anyproto/anytype-heart/pkg/lib/datastore"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/space/mock_space"
"github.com/anyproto/anytype-heart/tests/testutil"
)
func TestService_SaveDeviceInfo(t *testing.T) {
deviceObjectId := "deviceObjectId"
t.Run("save device in object", func(t *testing.T) {
// given
testDevice := &model.DeviceInfo{
Id: "id",
Name: "test",
}
devicesService := newFixture(t, deviceObjectId)
deviceObject := &editor.Page{SmartBlock: smarttest.New(deviceObjectId)}
state := deviceObject.NewState()
state.AddDevice(testDevice)
err := deviceObject.Apply(state)
assert.Nil(t, err)
// when
err = devicesService.SaveDeviceInfo(smartblock.ApplyInfo{State: deviceObject.NewState()})
// then
assert.Nil(t, err)
assert.NotNil(t, deviceObject.NewState().GetDevice("id"))
deviceInfos, err := devicesService.store.ListDevices()
assert.Nil(t, err)
assert.Contains(t, deviceInfos, testDevice)
})
t.Run("save device in object, device exist", func(t *testing.T) {
// given
testDevice := &model.DeviceInfo{
Id: "id",
Name: "test",
}
devicesService := newFixture(t, deviceObjectId)
deviceObject := &editor.Page{SmartBlock: smarttest.New(deviceObjectId)}
testDevice1 := &model.DeviceInfo{
Id: "id",
Name: "test1",
}
state := deviceObject.NewState()
state.AddDevice(testDevice)
err := deviceObject.Apply(state)
assert.Nil(t, err)
// when
err = devicesService.SaveDeviceInfo(smartblock.ApplyInfo{State: deviceObject.NewState()})
err = devicesService.SaveDeviceInfo(smartblock.ApplyInfo{State: deviceObject.NewState()})
// then
assert.Nil(t, err)
assert.NotNil(t, deviceObject.NewState().GetDevice("id"))
assert.Equal(t, "test", deviceObject.NewState().GetDevice("id").Name)
deviceInfos, err := devicesService.store.ListDevices()
assert.Nil(t, err)
assert.Contains(t, deviceInfos, testDevice)
assert.NotContains(t, deviceInfos, testDevice1)
})
}
func TestService_UpdateName(t *testing.T) {
deviceObjectId := "deviceObjectId"
techSpaceId := "techSpaceId"
t.Run("update name, device not exist", func(t *testing.T) {
// given
devicesService := newFixture(t, deviceObjectId)
virtualSpace := clientspace.NewVirtualSpace(techSpaceId, clientspace.VirtualSpaceDeps{})
devicesService.mockSpaceService.EXPECT().Get(context.Background(), techSpaceId).Return(virtualSpace, nil)
devicesService.mockSpaceService.EXPECT().TechSpaceId().Return(techSpaceId)
deviceObject := &editor.Page{SmartBlock: smarttest.New(deviceObjectId)}
mockCache := mock_objectcache.NewMockCache(t)
mockCache.EXPECT().GetObject(context.Background(), deviceObjectId).Return(deviceObject, nil)
virtualSpace.Cache = mockCache
// when
err := devicesService.UpdateName(context.Background(), "id", "new name")
// then
assert.Nil(t, err)
assert.NotNil(t, deviceObject.NewState().GetDevice("id"))
assert.Equal(t, "new name", deviceObject.NewState().GetDevice("id").Name)
deviceInfos, err := devicesService.store.ListDevices()
assert.Nil(t, err)
assert.Contains(t, deviceInfos, &model.DeviceInfo{
Id: "id",
Name: "new name",
})
})
t.Run("update name, device exists", func(t *testing.T) {
// given
testDevice := &model.DeviceInfo{
Id: "id",
Name: "test",
}
devicesService := newFixture(t, deviceObjectId)
virtualSpace := clientspace.NewVirtualSpace(techSpaceId, clientspace.VirtualSpaceDeps{})
devicesService.mockSpaceService.EXPECT().Get(context.Background(), techSpaceId).Return(virtualSpace, nil)
devicesService.mockSpaceService.EXPECT().TechSpaceId().Return(techSpaceId)
deviceObject := &editor.Page{SmartBlock: smarttest.New(deviceObjectId)}
mockCache := mock_objectcache.NewMockCache(t)
mockCache.EXPECT().GetObject(context.Background(), deviceObjectId).Return(deviceObject, nil)
state := deviceObject.NewState()
state.AddDevice(testDevice)
err := deviceObject.Apply(state)
assert.Nil(t, err)
virtualSpace.Cache = mockCache
err = devicesService.SaveDeviceInfo(smartblock.ApplyInfo{State: deviceObject.NewState()})
assert.Nil(t, err)
// when
err = devicesService.UpdateName(context.Background(), "id", "new name")
// then
assert.Nil(t, err)
assert.NotNil(t, deviceObject.NewState().GetDevice("id"))
assert.Equal(t, "new name", deviceObject.NewState().GetDevice("id").Name)
deviceInfos, err := devicesService.store.ListDevices()
assert.Nil(t, err)
assert.NotContains(t, deviceInfos, testDevice)
testDevice.Name = "new name"
assert.Contains(t, deviceInfos, testDevice)
})
}
func TestService_ListDevices(t *testing.T) {
deviceObjectId := "deviceObjectId"
t.Run("list devices, no devices", func(t *testing.T) {
// given
devicesService := newFixture(t, deviceObjectId)
// when
close(devicesService.finishLoad)
devicesList, err := devicesService.ListDevices(context.Background())
// then
assert.Nil(t, err)
assert.Len(t, devicesList, 0)
})
t.Run("list devices", func(t *testing.T) {
// given
testDevice := &model.DeviceInfo{
Id: "id",
Name: "test",
}
testDevice1 := &model.DeviceInfo{
Id: "id1",
Name: "test1",
}
devicesService := newFixture(t, deviceObjectId)
deviceObject := &editor.Page{SmartBlock: smarttest.New(deviceObjectId)}
state := deviceObject.NewState()
state.AddDevice(testDevice)
state.AddDevice(testDevice1)
err := deviceObject.Apply(state)
assert.Nil(t, err)
err = devicesService.SaveDeviceInfo(smartblock.ApplyInfo{State: deviceObject.NewState()})
assert.Nil(t, err)
// when
close(devicesService.finishLoad)
devicesList, err := devicesService.ListDevices(context.Background())
// then
assert.Nil(t, err)
assert.Len(t, devicesList, 2)
assert.Equal(t, devicesList[0].Id, "id")
assert.Equal(t, devicesList[1].Id, "id1")
})
}
func TestService_loadDevices(t *testing.T) {
deviceObjectId := "deviceObjectId"
techSpaceId := "techSpaceId"
ctx := context.Background()
t.Run("loadDevices, device object not exist", func(t *testing.T) {
// given
devicesService := newFixture(t, deviceObjectId)
virtualSpace := clientspace.NewVirtualSpace(techSpaceId, clientspace.VirtualSpaceDeps{})
devicesService.mockSpaceService.EXPECT().GetTechSpace(ctx).Return(virtualSpace, nil)
deviceObject := &editor.Page{SmartBlock: smarttest.New(deviceObjectId)}
mockCache := mock_objectcache.NewMockCache(t)
mockCache.EXPECT().GetObject(mock.Anything, deviceObjectId).Return(nil, fmt.Errorf("error"))
mockCache.EXPECT().DeriveTreeObject(mock.Anything, mock.Anything).Return(deviceObject, nil)
mockCache.EXPECT().DeriveObjectID(mock.Anything, mock.Anything).Return(deviceObjectId, nil)
virtualSpace.Cache = mockCache
// when
devicesService.loadDevices(ctx)
// then
assert.NotNil(t, deviceObject.NewState().GetDevice(devicesService.wallet.GetDevicePrivkey().GetPublic().PeerId()))
})
t.Run("loadDevices, device object exist", func(t *testing.T) {
// given
devicesService := newFixture(t, deviceObjectId)
virtualSpace := clientspace.NewVirtualSpace(techSpaceId, clientspace.VirtualSpaceDeps{})
devicesService.mockSpaceService.EXPECT().GetTechSpace(ctx).Return(virtualSpace, nil)
deviceObject := &editor.Page{SmartBlock: smarttest.New(deviceObjectId)}
mockCache := mock_objectcache.NewMockCache(t)
mockCache.EXPECT().DeriveObjectID(mock.Anything, mock.Anything).Return(deviceObjectId, nil)
mockCache.EXPECT().GetObject(mock.Anything, deviceObjectId).Return(deviceObject, nil)
virtualSpace.Cache = mockCache
// when
devicesService.loadDevices(ctx)
// then
assert.NotNil(t, deviceObject.NewState().GetDevice(devicesService.wallet.GetDevicePrivkey().GetPublic().PeerId()))
})
t.Run("loadDevices, save devices from derived objects", func(t *testing.T) {
// given
devicesService := newFixture(t, deviceObjectId)
virtualSpace := clientspace.NewVirtualSpace(techSpaceId, clientspace.VirtualSpaceDeps{})
devicesService.mockSpaceService.EXPECT().GetTechSpace(ctx).Return(virtualSpace, nil)
deviceObject := &editor.Page{SmartBlock: smarttest.New(deviceObjectId)}
mockCache := mock_objectcache.NewMockCache(t)
mockCache.EXPECT().DeriveObjectID(mock.Anything, mock.Anything).Return(deviceObjectId, nil)
mockCache.EXPECT().GetObject(mock.Anything, deviceObjectId).Return(deviceObject, nil)
virtualSpace.Cache = mockCache
state := deviceObject.NewState()
state.AddDevice(&model.DeviceInfo{
Id: "test",
Name: "test",
IsConnected: true,
})
state.AddDevice(&model.DeviceInfo{
Id: "test1",
Name: "test1",
})
err := deviceObject.Apply(state)
assert.Nil(t, err)
deviceObject.AddHook(devicesService.SaveDeviceInfo, smartblock.HookAfterApply)
// when
devicesService.loadDevices(ctx)
// then
assert.NotNil(t, deviceObject.NewState().GetDevice(devicesService.wallet.GetDevicePrivkey().GetPublic().PeerId()))
listDevices, err := devicesService.store.ListDevices()
assert.Nil(t, err)
assert.Len(t, listDevices, 3)
})
}
func TestService_Init(t *testing.T) {
t.Run("successfully started and closed service", func(t *testing.T) {
// given
deviceObjectId := "deviceObjectId"
ctx := context.Background()
techSpaceId := "techSpaceId"
devicesService := newFixture(t, deviceObjectId)
virtualSpace := clientspace.NewVirtualSpace(techSpaceId, clientspace.VirtualSpaceDeps{})
devicesService.mockSpaceService.EXPECT().GetTechSpace(mock.Anything).Return(virtualSpace, nil).Maybe()
deviceObject := &editor.Page{SmartBlock: smarttest.New(deviceObjectId)}
mockCache := mock_objectcache.NewMockCache(t)
mockCache.EXPECT().GetObject(mock.Anything, deviceObjectId).Return(deviceObject, nil).Maybe()
mockCache.EXPECT().DeriveTreeObject(mock.Anything, mock.Anything).Return(nil, treestorage.ErrTreeExists).Maybe()
mockCache.EXPECT().DeriveObjectID(mock.Anything, mock.Anything).Return(deviceObjectId, nil).Maybe()
virtualSpace.Cache = mockCache
// when
assert.Nil(t, devicesService.Run(ctx))
// then
assert.Nil(t, devicesService.Close(ctx))
})
}
type deviceFixture struct {
*devices
mockSpaceService *mock_space.MockService
mockCache *mock_objectcache.MockCache
wallet wallet2.Wallet
db datastore.Datastore
}
func newFixture(t *testing.T, deviceObjectId string) *deviceFixture {
mockSpaceService := mock_space.NewMockService(t)
mockCache := mock_objectcache.NewMockCache(t)
wallet := wallet2.NewWithRepoDirAndRandomKeys(os.TempDir())
db, err := datastore.NewInMemory()
assert.Nil(t, err)
df := &deviceFixture{
mockSpaceService: mockSpaceService,
mockCache: mockCache,
wallet: wallet,
devices: &devices{deviceObjectId: deviceObjectId, finishLoad: make(chan struct{})},
db: db,
}
a := &app.App{}
a.Register(testutil.PrepareMock(context.Background(), a, mockSpaceService)).
Register(wallet).
Register(db)
err = wallet.Init(a)
assert.Nil(t, err)
err = df.Init(a)
assert.Nil(t, err)
return df
}

100
core/device/store.go Normal file
View file

@ -0,0 +1,100 @@
package device
import (
"github.com/dgraph-io/badger/v4"
"github.com/gogo/protobuf/proto"
ds "github.com/ipfs/go-datastore"
"github.com/anyproto/anytype-heart/pkg/lib/localstore"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/badgerhelper"
)
const devicesInfo = "devices"
var deviceInfo = ds.NewKey("/" + devicesInfo + "/info")
type Store interface {
SaveDevice(device *model.DeviceInfo) error
ListDevices() ([]*model.DeviceInfo, error)
UpdateDeviceName(id, name string) error
}
type deviceStore struct {
db *badger.DB
}
func NewStore(db *badger.DB) Store {
return &deviceStore{db: db}
}
func (n *deviceStore) SaveDevice(device *model.DeviceInfo) error {
return n.db.Update(func(txn *badger.Txn) error {
_, err := txn.Get(deviceInfo.ChildString(device.Id).Bytes())
if err != nil && !badgerhelper.IsNotFound(err) {
return err
}
if badgerhelper.IsNotFound(err) {
infoRaw, err := device.Marshal()
if err != nil {
return err
}
return txn.Set(deviceInfo.ChildString(device.Id).Bytes(), infoRaw)
}
return nil
})
}
func (n *deviceStore) ListDevices() ([]*model.DeviceInfo, error) {
return badgerhelper.ViewTxnWithResult(n.db, func(txn *badger.Txn) ([]*model.DeviceInfo, error) {
keys := localstore.GetKeys(txn, deviceInfo.String(), 0)
devicesIds, err := localstore.GetLeavesFromResults(keys)
if err != nil {
return nil, err
}
deviceInfos := make([]*model.DeviceInfo, 0, len(devicesIds))
for _, id := range devicesIds {
info := deviceInfo.ChildString(id)
device, err := badgerhelper.GetValueTxn(txn, info.Bytes(), unmarshalDeviceInfo)
if badgerhelper.IsNotFound(err) {
continue
}
deviceInfos = append(deviceInfos, device)
}
return deviceInfos, nil
})
}
func (n *deviceStore) UpdateDeviceName(id, name string) error {
return n.db.Update(func(txn *badger.Txn) error {
item, err := txn.Get(deviceInfo.ChildString(id).Bytes())
if err != nil && !badgerhelper.IsNotFound(err) {
return err
}
var info *model.DeviceInfo
if badgerhelper.IsNotFound(err) {
info = &model.DeviceInfo{
Id: id,
Name: name,
}
} else {
if err = item.Value(func(val []byte) error {
info, err = unmarshalDeviceInfo(val)
return err
}); err != nil {
return err
}
info.Name = name
}
infoRaw, err := info.Marshal()
if err != nil {
return err
}
return txn.Set(deviceInfo.ChildString(id).Bytes(), infoRaw)
})
}
func unmarshalDeviceInfo(raw []byte) (*model.DeviceInfo, error) {
v := &model.DeviceInfo{}
return v, proto.Unmarshal(raw, v)
}

145
core/device/store_test.go Normal file
View file

@ -0,0 +1,145 @@
package device
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anyproto/anytype-heart/pkg/lib/datastore"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
func TestDeviceStore_SaveDevice(t *testing.T) {
t.Run("device exist: not save it again", func(t *testing.T) {
// given
memory, err := datastore.NewInMemory()
assert.Nil(t, err)
storage, err := memory.LocalStorage()
assert.Nil(t, err)
store := NewStore(storage)
testInfo1 := &model.DeviceInfo{Id: "test", Name: "test"}
testInfo2 := &model.DeviceInfo{Id: "test", Name: "test"}
err = store.SaveDevice(testInfo1)
assert.Nil(t, err)
// when
err = store.SaveDevice(testInfo2)
// then
assert.Nil(t, err)
listDevices, err := store.ListDevices()
assert.Nil(t, err)
assert.Len(t, listDevices, 1)
})
t.Run("device not exist: save it", func(t *testing.T) {
// given
memory, err := datastore.NewInMemory()
assert.Nil(t, err)
storage, err := memory.LocalStorage()
assert.Nil(t, err)
store := NewStore(storage)
testInfo1 := &model.DeviceInfo{Id: "test", Name: "test"}
// when
err = store.SaveDevice(testInfo1)
// then
assert.Nil(t, err)
listDevices, err := store.ListDevices()
assert.Nil(t, err)
assert.Len(t, listDevices, 1)
})
}
func TestDeviceStore_ListDevices(t *testing.T) {
t.Run("list devices: no devices", func(t *testing.T) {
// given
memory, err := datastore.NewInMemory()
assert.Nil(t, err)
storage, err := memory.LocalStorage()
assert.Nil(t, err)
store := NewStore(storage)
// when
devices, err := store.ListDevices()
// then
assert.Nil(t, err)
assert.Len(t, devices, 0)
})
t.Run("list devices: 2 devices", func(t *testing.T) {
// given
memory, err := datastore.NewInMemory()
assert.Nil(t, err)
storage, err := memory.LocalStorage()
assert.Nil(t, err)
store := NewStore(storage)
testInfo1 := &model.DeviceInfo{Id: "test", Name: "test"}
testInfo2 := &model.DeviceInfo{Id: "test1", Name: "test"}
err = store.SaveDevice(testInfo1)
assert.Nil(t, err)
err = store.SaveDevice(testInfo2)
assert.Nil(t, err)
// when
devices, err := store.ListDevices()
// then
assert.Nil(t, err)
assert.Len(t, devices, 2)
})
}
func TestDeviceStore_UpdateDeviceName(t *testing.T) {
t.Run("update device: device not exist - save it", func(t *testing.T) {
// given
memory, err := datastore.NewInMemory()
assert.Nil(t, err)
storage, err := memory.LocalStorage()
assert.Nil(t, err)
store := NewStore(storage)
// when
err = store.UpdateDeviceName("id", "test")
// then
assert.Nil(t, err)
listDevices, err := store.ListDevices()
assert.Nil(t, err)
assert.Len(t, listDevices, 1)
assert.Contains(t, listDevices, &model.DeviceInfo{
Id: "id",
Name: "test",
})
})
t.Run("update device: device exists - update it", func(t *testing.T) {
// given
memory, err := datastore.NewInMemory()
assert.Nil(t, err)
storage, err := memory.LocalStorage()
assert.Nil(t, err)
store := NewStore(storage)
testInfo1 := &model.DeviceInfo{Id: "id", Name: "test"}
err = store.SaveDevice(testInfo1)
assert.Nil(t, err)
// when
err = store.UpdateDeviceName("id", "test1")
// then
assert.Nil(t, err)
listDevices, err := store.ListDevices()
assert.Nil(t, err)
assert.Len(t, listDevices, 1)
assert.Contains(t, listDevices, &model.DeviceInfo{
Id: "id",
Name: "test1",
})
})
}

40
core/devices.go Normal file
View file

@ -0,0 +1,40 @@
package core
import (
"context"
"github.com/anyproto/anytype-heart/core/device"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
)
func (mw *Middleware) DeviceSetName(cctx context.Context, req *pb.RpcDeviceSetNameRequest) *pb.RpcDeviceSetNameResponse {
response := func(code pb.RpcDeviceSetNameResponseErrorCode, err error) *pb.RpcDeviceSetNameResponse {
m := &pb.RpcDeviceSetNameResponse{Error: &pb.RpcDeviceSetNameResponseError{Code: code}}
if err != nil {
m.Error.Description = err.Error()
}
return m
}
err := getService[device.Service](mw).UpdateName(cctx, req.DeviceId, req.Name)
if err != nil {
return response(pb.RpcDeviceSetNameResponseError_UNKNOWN_ERROR, err)
}
return response(pb.RpcDeviceSetNameResponseError_NULL, nil)
}
func (mw *Middleware) DeviceList(cctx context.Context, _ *pb.RpcDeviceListRequest) *pb.RpcDeviceListResponse {
response := func(code pb.RpcDeviceListResponseErrorCode, devices []*model.DeviceInfo, err error) *pb.RpcDeviceListResponse {
m := &pb.RpcDeviceListResponse{Error: &pb.RpcDeviceListResponseError{Code: code}}
if err != nil {
m.Error.Description = err.Error()
}
m.Devices = devices
return m
}
devices, err := getService[device.Service](mw).ListDevices(cctx)
if err != nil {
return response(pb.RpcDeviceListResponseError_UNKNOWN_ERROR, devices, err)
}
return response(pb.RpcDeviceListResponseError_NULL, devices, nil)
}

View file

@ -7,7 +7,7 @@ import (
const (
// ObjectPathSeparator is the separator between object id and block id or relation key
objectPathSeparator = "/"
ObjectPathSeparator = "/"
blockPrefix = "b"
relationPrefix = "r"
)
@ -21,10 +21,10 @@ type ObjectPath struct {
// String returns the full path, e.g. "objectId-b-blockId" or "objectId-r-relationKey"
func (o ObjectPath) String() string {
if o.HasBlock() {
return strings.Join([]string{o.ObjectId, blockPrefix, o.BlockId}, objectPathSeparator)
return strings.Join([]string{o.ObjectId, blockPrefix, o.BlockId}, ObjectPathSeparator)
}
if o.HasRelation() {
return strings.Join([]string{o.ObjectId, relationPrefix, o.RelationKey}, objectPathSeparator)
return strings.Join([]string{o.ObjectId, relationPrefix, o.RelationKey}, ObjectPathSeparator)
}
return o.ObjectId
}
@ -32,10 +32,10 @@ func (o ObjectPath) String() string {
// ObjectRelativePath returns the relative path of the object without the object id prefix
func (o ObjectPath) ObjectRelativePath() string {
if o.HasBlock() {
return strings.Join([]string{blockPrefix, o.BlockId}, objectPathSeparator)
return strings.Join([]string{blockPrefix, o.BlockId}, ObjectPathSeparator)
}
if o.HasRelation() {
return strings.Join([]string{relationPrefix, o.RelationKey}, objectPathSeparator)
return strings.Join([]string{relationPrefix, o.RelationKey}, ObjectPathSeparator)
}
return ""
}
@ -67,7 +67,7 @@ func NewObjectPathWithRelation(objectId, relationKey string) ObjectPath {
}
func NewFromPath(path string) (ObjectPath, error) {
parts := strings.Split(path, objectPathSeparator)
parts := strings.Split(path, ObjectPathSeparator)
if len(parts) == 3 && parts[1] == blockPrefix {
return NewObjectPathWithBlock(parts[0], parts[2]), nil
}

View file

@ -1,51 +1,29 @@
package domain
type SyncType int32
const (
Objects SyncType = 0
Files SyncType = 1
)
type SpaceSyncStatus int32
const (
Synced SpaceSyncStatus = 0
Syncing SpaceSyncStatus = 1
Error SpaceSyncStatus = 2
Offline SpaceSyncStatus = 3
SpaceSyncStatusSynced SpaceSyncStatus = 0
SpaceSyncStatusSyncing SpaceSyncStatus = 1
SpaceSyncStatusError SpaceSyncStatus = 2
SpaceSyncStatusOffline SpaceSyncStatus = 3
SpaceSyncStatusUnknown SpaceSyncStatus = 4
)
type ObjectSyncStatus int32
const (
ObjectSynced ObjectSyncStatus = 0
ObjectSyncing ObjectSyncStatus = 1
ObjectError ObjectSyncStatus = 2
ObjectQueued ObjectSyncStatus = 3
ObjectSyncStatusSynced ObjectSyncStatus = 0
ObjectSyncStatusSyncing ObjectSyncStatus = 1
ObjectSyncStatusError ObjectSyncStatus = 2
ObjectSyncStatusQueued ObjectSyncStatus = 3
)
type SyncError int32
const (
Null SyncError = 0
StorageLimitExceed SyncError = 1
IncompatibleVersion SyncError = 2
NetworkError SyncError = 3
SyncErrorNull SyncError = 0
SyncErrorIncompatibleVersion SyncError = 2
SyncErrorNetworkError SyncError = 3
SyncErrorOversized SyncError = 4
)
type SpaceSync struct {
SpaceId string
Status SpaceSyncStatus
SyncError SyncError
SyncType SyncType
}
func MakeSyncStatus(spaceId string, status SpaceSyncStatus, syncError SyncError, syncType SyncType) *SpaceSync {
return &SpaceSync{
SpaceId: spaceId,
Status: status,
SyncError: syncError,
SyncType: syncType,
}
}

View file

@ -23,6 +23,7 @@ var smartBlockTypeToKey = map[smartblock.SmartBlockType]string{
smartblock.SmartBlockTypeFileObject: "file", // For migration purposes only
smartblock.SmartBlockTypePage: "page", // For migration purposes only, used for old profile data migration
smartblock.SmartBlockTypeNotificationObject: "notification",
smartblock.SmartBlockTypeDevicesObject: "devices",
}
// UniqueKey is unique key composed of two parts: smartblock type and internal key.

View file

@ -37,6 +37,8 @@ type indexer struct {
indexQueue *mb.MB[indexRequest]
isQueuedLock sync.RWMutex
isQueued map[domain.FullID]struct{}
closeWg *sync.WaitGroup
}
func (s *service) newIndexer() *indexer {
@ -47,6 +49,8 @@ func (s *service) newIndexer() *indexer {
indexQueue: mb.New[indexRequest](0),
isQueued: make(map[domain.FullID]struct{}),
closeWg: &sync.WaitGroup{},
}
ind.initQuery()
return ind
@ -54,12 +58,17 @@ func (s *service) newIndexer() *indexer {
func (ind *indexer) run() {
ind.indexCtx, ind.indexCancel = context.WithCancel(context.Background())
ind.closeWg.Add(1)
go ind.runIndexingProvider()
ind.closeWg.Add(1)
go ind.runIndexingWorker()
}
func (ind *indexer) close() error {
ind.indexCancel()
ind.closeWg.Wait()
return ind.indexQueue.Close()
}
@ -143,6 +152,8 @@ const indexingProviderPeriod = 60 * time.Second
// runIndexingProvider provides worker with job to do
func (ind *indexer) runIndexingProvider() {
defer ind.closeWg.Done()
ticker := time.NewTicker(indexingProviderPeriod)
run := func() {
if err := ind.addToQueueFromObjectStore(ind.indexCtx); err != nil {
@ -162,6 +173,8 @@ func (ind *indexer) runIndexingProvider() {
}
func (ind *indexer) runIndexingWorker() {
defer ind.closeWg.Done()
for {
select {
case <-ind.indexCtx.Done():
@ -217,7 +230,7 @@ func (ind *indexer) injectMetadataToState(ctx context.Context, st *state.State,
for k := range details.Fields {
keys = append(keys, domain.RelationKey(k))
}
st.AddBundledRelations(keys...)
st.AddBundledRelationLinks(keys...)
details = pbtypes.StructMerge(prevDetails, details, false)
st.SetDetails(details)

View file

@ -3,6 +3,7 @@ package fileobject
import (
"context"
"fmt"
"sync"
"time"
"github.com/anyproto/any-sync/app"
@ -85,6 +86,8 @@ type service struct {
resolverRetryStartDelay time.Duration
resolverRetryMaxDelay time.Duration
closeWg *sync.WaitGroup
}
func New(
@ -94,6 +97,7 @@ func New(
return &service{
resolverRetryStartDelay: resolverRetryStartDelay,
resolverRetryMaxDelay: resolverRetryMaxDelay,
closeWg: &sync.WaitGroup{},
}
}
@ -140,7 +144,10 @@ func (s *service) Init(a *app.App) error {
}
func (s *service) Run(_ context.Context) error {
s.closeWg.Add(1)
go func() {
defer s.closeWg.Done()
err := s.ensureNotSyncedFilesAddedToQueue()
if err != nil {
log.Errorf("ensure not synced files added to queue: %v", err)
@ -204,6 +211,7 @@ func (s *service) EnsureFileAddedToSyncQueue(id domain.FullID, details *types.St
}
func (s *service) Close(ctx context.Context) error {
s.closeWg.Wait()
return s.indexer.close()
}
@ -304,8 +312,8 @@ func (s *service) makeInitialDetails(fileId domain.FileId, origin objectorigin.O
// Use general file layout. It will be changed for proper layout after indexing
bundle.RelationKeyLayout.String(): pbtypes.Int64(int64(model.ObjectType_file)),
bundle.RelationKeyFileIndexingStatus.String(): pbtypes.Int64(int64(model.FileIndexingStatus_NotIndexed)),
bundle.RelationKeySyncStatus.String(): pbtypes.Int64(int64(domain.ObjectQueued)),
bundle.RelationKeySyncError.String(): pbtypes.Int64(int64(domain.Null)),
bundle.RelationKeySyncStatus.String(): pbtypes.Int64(int64(domain.ObjectSyncStatusQueued)),
bundle.RelationKeySyncError.String(): pbtypes.Int64(int64(domain.SyncErrorNull)),
bundle.RelationKeyFileBackupStatus.String(): pbtypes.Int64(int64(filesyncstatus.Queued)),
},
}

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