mirror of
https://github.com/anyproto/anytype-heart.git
synced 2025-06-09 17:44:59 +09:00
GO-4779 Merge branch 'GO-4146-new-spacestore' of github.com:anyproto/anytype-heart into go-4779-show-last-message-preview-from-chat-in-vault
This commit is contained in:
commit
a0059dda9e
217 changed files with 35458 additions and 5350 deletions
2
.github/workflows/cla.yml
vendored
2
.github/workflows/cla.yml
vendored
|
@ -10,7 +10,7 @@ permissions:
|
|||
contents: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
|
||||
jobs:
|
||||
cla-check:
|
||||
uses: anyproto/open/.github/workflows/cla.yml@main
|
||||
|
|
241
.github/workflows/nightly.yml
vendored
Normal file
241
.github/workflows/nightly.yml
vendored
Normal file
|
@ -0,0 +1,241 @@
|
|||
name: Nightly Builds
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'nightly*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
channel:
|
||||
description: electron.builder channel
|
||||
required: true
|
||||
default: alpha
|
||||
type: choice
|
||||
options:
|
||||
- alpha
|
||||
- beta
|
||||
# schedule:
|
||||
# - cron: '0 0 * * *' # every day at midnight
|
||||
# filters:
|
||||
# branches:
|
||||
# include:
|
||||
# - 'nightly-ci-test'
|
||||
|
||||
permissions:
|
||||
actions: 'write'
|
||||
packages: 'write'
|
||||
contents: 'write'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: 'macos-14'
|
||||
steps:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.23.2
|
||||
check-latest: true
|
||||
|
||||
- name: Setup GO
|
||||
run: |
|
||||
go version
|
||||
echo GOPATH=$(go env GOPATH) >> $GITHUB_ENV
|
||||
echo GOBIN=$(go env GOPATH)/bin >> $GITHUB_ENV
|
||||
echo $(go env GOPATH)/bin >> $GITHUB_PATH
|
||||
|
||||
- name: Install brew and node deps
|
||||
run: |
|
||||
curl https://raw.githubusercontent.com/Homebrew/homebrew-core/31b24d65a7210ea0a5689d5ad00dd8d1bf5211db/Formula/protobuf.rb --output protobuf.rb
|
||||
curl https://raw.githubusercontent.com/Homebrew/homebrew-core/d600b1f7119f6e6a4e97fb83233b313b0468b7e4/Formula/s/swift-protobuf.rb --output swift-protobuf.rb
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 brew install ./protobuf.rb
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 brew install --ignore-dependencies ./swift-protobuf.rb
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 brew install mingw-w64
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 brew install grpcurl
|
||||
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 brew tap messense/macos-cross-toolchains && brew install x86_64-unknown-linux-musl && brew install aarch64-unknown-linux-musl
|
||||
npm i -g node-gyp
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Nightly mode env settings
|
||||
shell: bash
|
||||
run: |
|
||||
# choice channel name {{
|
||||
if [[ -z "${{ github.event.inputs.channel }}" ]]; then
|
||||
# choice default value for channel from ref name
|
||||
if echo "${{ github.ref_name }}" | grep -q "beta"; then
|
||||
CHANNEL="beta"
|
||||
else
|
||||
CHANNEL="alpha"
|
||||
fi
|
||||
else
|
||||
CHANNEL="${{github.event.inputs.channel}}"
|
||||
fi
|
||||
echo "CHANNEL=$CHANNEL" >> $GITHUB_ENV
|
||||
# }}
|
||||
# choice s3 bucket for publishing {{
|
||||
if [[ "$CHANNEL" == "beta" ]]; then
|
||||
S3_BUCKET="${{secrets.NIGHTLY_AWS_S3_BUCKET_BETA}}"
|
||||
else
|
||||
S3_BUCKET="${{secrets.NIGHTLY_AWS_S3_BUCKET}}"
|
||||
fi
|
||||
echo "S3_BUCKET=$S3_BUCKET" >> $GITHUB_ENV
|
||||
# }}
|
||||
|
||||
- name: Set env vars
|
||||
env:
|
||||
UNSPLASH_KEY: ${{ secrets.UNSPLASH_KEY }}
|
||||
INHOUSE_KEY: ${{ secrets.INHOUSE_KEY }}
|
||||
run: |
|
||||
GIT_SUMMARY=$(git describe --tags --always)
|
||||
echo "FLAGS=-X github.com/anyproto/anytype-heart/util/vcs.GitSummary=${GIT_SUMMARY} -X github.com/anyproto/anytype-heart/metrics.DefaultInHouseKey=${INHOUSE_KEY} -X github.com/anyproto/anytype-heart/util/unsplash.DefaultToken=${UNSPLASH_KEY}" >> $GITHUB_ENV
|
||||
|
||||
VERSION="nightly"
|
||||
echo "${{ secrets.STAGING_NODE_CONF }}" > ./core/anytype/config/nodes/custom.yml
|
||||
echo BUILD_TAG_NETWORK=envnetworkcustom >> $GITHUB_ENV
|
||||
|
||||
echo VERSION=${VERSION} >> $GITHUB_ENV
|
||||
echo GOPRIVATE=github.com/anyproto >> $GITHUB_ENV
|
||||
echo $(pwd)/deps >> $GITHUB_PATH
|
||||
git config --global url."https://${{ secrets.ANYTYPE_PAT }}@github.com/".insteadOf "https://github.com/"
|
||||
|
||||
- name: Go mod download
|
||||
run: go mod download
|
||||
|
||||
- name: install protoc
|
||||
run: make setup-protoc
|
||||
|
||||
- name: setup go
|
||||
run: |
|
||||
make setup-go
|
||||
make setup-gomobile
|
||||
which gomobile
|
||||
|
||||
- name: Cross-compile library mac/win/linux
|
||||
run: |
|
||||
echo $FLAGS
|
||||
mkdir -p .release
|
||||
echo $SDKROOT
|
||||
GOOS="darwin" CGO_CFLAGS="-mmacosx-version-min=11" MACOSX_DEPLOYMENT_TARGET=11.0 GOARCH="amd64" CGO_ENABLED="1" go build -tags="$BUILD_TAG_NETWORK nographviz nowatchdog nosigar nomutexdeadlockdetector" -ldflags="$FLAGS" -o darwin-amd64 github.com/anyproto/anytype-heart/cmd/grpcserver
|
||||
export SDKROOT=$(xcrun --sdk macosx --show-sdk-path)
|
||||
echo $SDKROOT
|
||||
GOOS="darwin" CGO_CFLAGS="-mmacosx-version-min=11" MACOSX_DEPLOYMENT_TARGET=11.0 GOARCH="arm64" CGO_ENABLED="1" go build -tags="$BUILD_TAG_NETWORK nographviz nowatchdog nosigar nomutexdeadlockdetector" -ldflags="$FLAGS" -o darwin-arm64 github.com/anyproto/anytype-heart/cmd/grpcserver
|
||||
GOOS="windows" GOARCH="amd64" CGO_ENABLED="1" CC="x86_64-w64-mingw32-gcc" CXX="x86_64-w64-mingw32-g++" go build -tags="$BUILD_TAG_NETWORK nographviz nowatchdog nosigar nomutexdeadlockdetector noheic" -ldflags="$FLAGS -linkmode external -extldflags=-static" -o windows-amd64 github.com/anyproto/anytype-heart/cmd/grpcserver
|
||||
GOOS="linux" GOARCH="amd64" CGO_ENABLED="1" CC="x86_64-linux-musl-gcc" go build -tags="$BUILD_TAG_NETWORK nographviz nowatchdog nosigar nomutexdeadlockdetector noheic" -ldflags="$FLAGS -linkmode external -extldflags '-static -Wl,-z stack-size=1000000'" -o linux-amd64 github.com/anyproto/anytype-heart/cmd/grpcserver
|
||||
GOOS="linux" GOARCH="arm64" CGO_ENABLED="1" CC="aarch64-linux-musl-gcc" go build -tags="$BUILD_TAG_NETWORK nographviz nowatchdog nosigar nomutexdeadlockdetector noheic" -ldflags="$FLAGS -linkmode external" -o linux-arm64 github.com/anyproto/anytype-heart/cmd/grpcserver
|
||||
ls -lha .
|
||||
|
||||
- name: Make JS protos
|
||||
run: |
|
||||
make protos-js
|
||||
mv dist/js/pb protobuf
|
||||
mkdir -p protobuf/protos
|
||||
cp pb/protos/*.proto ./protobuf/protos
|
||||
cp pb/protos/service/*.proto ./protobuf/protos
|
||||
cp pkg/lib/pb/model/protos/*.proto ./protobuf/protos
|
||||
|
||||
- name: Add system relations/types jsons
|
||||
run: |
|
||||
mkdir -p json/
|
||||
cp pkg/lib/bundle/systemRelations.json ./json
|
||||
cp pkg/lib/bundle/systemTypes.json ./json
|
||||
cp pkg/lib/bundle/internalRelations.json ./json
|
||||
cp pkg/lib/bundle/internalTypes.json ./json
|
||||
|
||||
- name: Pack server win
|
||||
run: |
|
||||
declare -a arr=("windows-amd64")
|
||||
for i in "${arr[@]}"; do
|
||||
OSARCH=${i%.*}
|
||||
cp ./${i}* ./grpc-server.exe
|
||||
zip -r js_${VERSION}_${OSARCH}.zip grpc-server.exe protobuf json
|
||||
mv js_${VERSION}_${OSARCH}.zip .release/
|
||||
done
|
||||
|
||||
- name: Pack server unix
|
||||
run: |
|
||||
declare -a arr=("darwin-amd64" "darwin-arm64" "linux-amd64")
|
||||
for i in "${arr[@]}"; do
|
||||
OSARCH=${i%.*}
|
||||
cp ./${i}* ./grpc-server
|
||||
tar -czf js_${VERSION}_${OSARCH}.tar.gz grpc-server protobuf json
|
||||
mv js_${VERSION}_${OSARCH}.tar.gz .release/
|
||||
done
|
||||
|
||||
- name: Make swift protos
|
||||
run: |
|
||||
mkdir -p .release
|
||||
make protos-swift
|
||||
rm -rf protobuf
|
||||
mv dist/ios/protobuf protobuf
|
||||
mkdir -p protobuf/protos
|
||||
cp pb/protos/*.proto ./protobuf/protos
|
||||
cp pb/protos/service/*.proto ./protobuf/protos
|
||||
cp pkg/lib/pb/model/protos/*.proto ./protobuf/protos
|
||||
|
||||
- name: Add system relations/types jsons
|
||||
run: |
|
||||
mkdir -p json/
|
||||
cp pkg/lib/bundle/systemRelations.json ./json
|
||||
cp pkg/lib/bundle/relations.json ./json
|
||||
cp pkg/lib/bundle/systemTypes.json ./json
|
||||
cp pkg/lib/bundle/internalRelations.json ./json
|
||||
cp pkg/lib/bundle/internalTypes.json ./json
|
||||
|
||||
- name: Compile ios lib
|
||||
run: |
|
||||
go install github.com/vektra/mockery/v2@v2.47.0
|
||||
go install go.uber.org/mock/mockgen@v0.5.0
|
||||
make test-deps
|
||||
gomobile bind -tags "$BUILD_TAG_NETWORK nogrpcserver gomobile nowatchdog nosigar nomutexdeadlockdetector timetzdata rasterizesvg" -ldflags "$FLAGS" -v -target=ios -o Lib.xcframework github.com/anyproto/anytype-heart/clientlibrary/service github.com/anyproto/anytype-heart/core || true
|
||||
mkdir -p dist/ios/ && mv Lib.xcframework dist/ios/
|
||||
go run cmd/iosrepack/main.go
|
||||
mv dist/ios/Lib.xcframework .
|
||||
gtar --exclude ".*" -czvf ios_framework.tar.gz Lib.xcframework protobuf json
|
||||
#gradle publish
|
||||
mv ios_framework.tar.gz .release/ios_framework_${VERSION}.tar.gz
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_USER: ${{ github.actor }}
|
||||
|
||||
- name: Make java protos
|
||||
run: |
|
||||
make protos-java
|
||||
rm -rf protobuf
|
||||
mv dist/android/pb protobuf
|
||||
mkdir -p protobuf/protos
|
||||
cp pb/protos/*.proto ./protobuf/protos
|
||||
cp pb/protos/service/*.proto ./protobuf/protos
|
||||
cp pkg/lib/pb/model/protos/*.proto ./protobuf/protos
|
||||
|
||||
- name: Add system relations/types jsons
|
||||
run: |
|
||||
mkdir -p json/
|
||||
cp pkg/lib/bundle/systemRelations.json ./json
|
||||
cp pkg/lib/bundle/systemTypes.json ./json
|
||||
cp pkg/lib/bundle/internalRelations.json ./json
|
||||
cp pkg/lib/bundle/internalTypes.json ./json
|
||||
|
||||
- name: Compile android lib
|
||||
run: |
|
||||
gomobile bind -tags "$BUILD_TAG_NETWORK nogrpcserver gomobile nowatchdog nosigar nomutexdeadlockdetector timetzdata rasterizesvg" -ldflags "$FLAGS" -v -target=android -androidapi 26 -o lib.aar github.com/anyproto/anytype-heart/clientlibrary/service github.com/anyproto/anytype-heart/core || true
|
||||
gtar --exclude ".*" -czvf android_lib_${VERSION}.tar.gz lib.aar protobuf json
|
||||
mv android_lib_${VERSION}.tar.gz .release/
|
||||
|
||||
# upload release artifacts to s3 {{
|
||||
- name: Install AWS CLI
|
||||
run: |
|
||||
if ! which aws; then
|
||||
brew install awscli
|
||||
fi
|
||||
aws --version
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.NIGHTLY_AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.NIGHTLY_AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: ${{ secrets.NIGHTLY_AWS_REGION }}
|
||||
- name: Upload build artifacts to S3
|
||||
run: |
|
||||
aws s3 cp .release/ s3://${{ env.S3_BUCKET }}/mw/ --recursive --acl public-read
|
||||
# }}
|
|
@ -19,6 +19,7 @@ packages:
|
|||
github.com/anyproto/anytype-heart/core/block/cache:
|
||||
interfaces:
|
||||
ObjectGetter:
|
||||
CachedObjectGetter:
|
||||
ObjectGetterComponent:
|
||||
github.com/anyproto/anytype-heart/pkg/lib/core:
|
||||
interfaces:
|
||||
|
@ -37,9 +38,6 @@ packages:
|
|||
dir: "{{.InterfaceDir}}"
|
||||
outpkg: "{{.PackageName}}"
|
||||
inpackage: true
|
||||
github.com/anyproto/anytype-heart/core/block/editor/lastused:
|
||||
interfaces:
|
||||
ObjectUsageUpdater:
|
||||
github.com/anyproto/anytype-heart/core/block/import/common:
|
||||
interfaces:
|
||||
Converter:
|
||||
|
@ -231,3 +229,6 @@ packages:
|
|||
github.com/anyproto/anytype-heart/core/identity:
|
||||
interfaces:
|
||||
Service:
|
||||
github.com/anyproto/anytype-heart/pb/service:
|
||||
interfaces:
|
||||
ClientCommandsServer:
|
||||
|
|
51
Makefile
51
Makefile
|
@ -10,13 +10,13 @@ export GOLANGCI_LINT_VERSION=1.58.1
|
|||
export CGO_CFLAGS=-Wno-deprecated-non-prototype -Wno-unknown-warning-option -Wno-deprecated-declarations -Wno-xor-used-as-pow -Wno-single-bit-bitfield-constant-conversion
|
||||
|
||||
ifndef $(GOPATH)
|
||||
GOPATH=$(shell go env GOPATH)
|
||||
export GOPATH
|
||||
GOPATH=$(shell go env GOPATH)
|
||||
export GOPATH
|
||||
endif
|
||||
|
||||
ifndef $(GOROOT)
|
||||
GOROOT=$(shell go env GOROOT)
|
||||
export GOROOT
|
||||
GOROOT=$(shell go env GOROOT)
|
||||
export GOROOT
|
||||
endif
|
||||
|
||||
DEPS_PATH := $(shell pwd)/deps
|
||||
|
@ -38,14 +38,14 @@ ifdef ANYENV
|
|||
@exit 1;
|
||||
endif
|
||||
@if [ -z "$$ANY_SYNC_NETWORK" ]; then \
|
||||
echo "Using the default production Any Sync Network"; \
|
||||
elif [ ! -e "$$ANY_SYNC_NETWORK" ]; then \
|
||||
echo "Network configuration file not found at $$ANY_SYNC_NETWORK"; \
|
||||
exit 1; \
|
||||
else \
|
||||
echo "Using Any Sync Network configuration at $$ANY_SYNC_NETWORK"; \
|
||||
cp $$ANY_SYNC_NETWORK $(CUSTOM_NETWORK_FILE); \
|
||||
fi
|
||||
echo "Using the default production Any Sync Network"; \
|
||||
elif [ ! -e "$$ANY_SYNC_NETWORK" ]; then \
|
||||
echo "Network configuration file not found at $$ANY_SYNC_NETWORK"; \
|
||||
exit 1; \
|
||||
else \
|
||||
echo "Using Any Sync Network configuration at $$ANY_SYNC_NETWORK"; \
|
||||
cp $$ANY_SYNC_NETWORK $(CUSTOM_NETWORK_FILE); \
|
||||
fi
|
||||
|
||||
setup-go: setup-network-config check-tantivy-version
|
||||
@echo 'Setting up go modules...'
|
||||
|
@ -102,7 +102,7 @@ build-js-addon:
|
|||
@cp dist/lib.a clientlibrary/jsaddon/lib.a
|
||||
@cp dist/lib.h clientlibrary/jsaddon/lib.h
|
||||
@cp clientlibrary/clib/bridge.h clientlibrary/jsaddon/bridge.h
|
||||
# Electron's version.
|
||||
# Electron's version.
|
||||
@export npm_config_target=12.0.4
|
||||
@export npm_config_arch=x64
|
||||
@export npm_config_target_arch=x64
|
||||
|
@ -197,7 +197,6 @@ setup-protoc-go:
|
|||
go build -o deps github.com/gogo/protobuf/protoc-gen-gogofast
|
||||
go build -o deps github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc
|
||||
|
||||
|
||||
setup-protoc-jsweb:
|
||||
@echo 'Installing grpc-web plugin...'
|
||||
@rm -rf deps/grpc-web
|
||||
|
@ -346,17 +345,17 @@ OUTPUT_DIR := deps/libs
|
|||
SHA_FILE = tantivity_sha256.txt
|
||||
|
||||
TANTIVY_LIBS := android-386.tar.gz \
|
||||
android-amd64.tar.gz \
|
||||
android-arm.tar.gz \
|
||||
android-arm64.tar.gz \
|
||||
darwin-amd64.tar.gz \
|
||||
darwin-arm64.tar.gz \
|
||||
ios-amd64.tar.gz \
|
||||
ios-arm64.tar.gz \
|
||||
ios-arm64-sim.tar.gz \
|
||||
linux-amd64-musl.tar.gz \
|
||||
linux-arm64-musl.tar.gz \
|
||||
windows-amd64.tar.gz
|
||||
android-amd64.tar.gz \
|
||||
android-arm.tar.gz \
|
||||
android-arm64.tar.gz \
|
||||
darwin-amd64.tar.gz \
|
||||
darwin-arm64.tar.gz \
|
||||
ios-amd64.tar.gz \
|
||||
ios-arm64.tar.gz \
|
||||
ios-arm64-sim.tar.gz \
|
||||
linux-amd64-musl.tar.gz \
|
||||
linux-arm64-musl.tar.gz \
|
||||
windows-amd64.tar.gz
|
||||
|
||||
define download_tantivy_lib
|
||||
curl -L -o $(OUTPUT_DIR)/$(1) https://github.com/$(REPO)/releases/download/$(TANTIVY_VERSION)/$(1)
|
||||
|
@ -404,4 +403,4 @@ check-tantivy-version:
|
|||
$(eval OLD_VERSION := $(shell [ -f $(OUTPUT_DIR)/.verified ] && cat $(OUTPUT_DIR)/.verified || echo ""))
|
||||
@if [ "$(TANTIVY_VERSION)" != "$(OLD_VERSION)" ]; then \
|
||||
$(MAKE) download-tantivy-all; \
|
||||
fi
|
||||
fi
|
||||
|
|
1
account_stop.json
Normal file
1
account_stop.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"method_name":"AccountStop","middle_time":0,"error_code":101,"description":"application is not running"}
|
|
@ -25,344 +25,348 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package
|
|||
func init() { proto.RegisterFile("pb/protos/service/service.proto", fileDescriptor_93a29dc403579097) }
|
||||
|
||||
var fileDescriptor_93a29dc403579097 = []byte{
|
||||
// 5387 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x9d, 0xdf, 0x6f, 0x24, 0x49,
|
||||
0x52, 0xf8, 0xd7, 0x2f, 0xdf, 0xfd, 0x52, 0xc7, 0x2d, 0xd0, 0x0b, 0xcb, 0xde, 0x72, 0x37, 0xbf,
|
||||
0x76, 0xc6, 0xf6, 0x8c, 0xed, 0xf6, 0xec, 0xcc, 0xce, 0xee, 0xe9, 0x0e, 0x09, 0x79, 0xec, 0xb1,
|
||||
0xd7, 0x9c, 0xed, 0x31, 0xee, 0xf6, 0x8c, 0xb4, 0x12, 0x12, 0xe9, 0xea, 0x74, 0xbb, 0x70, 0x75,
|
||||
0x65, 0x5d, 0x55, 0x76, 0x7b, 0xfa, 0x10, 0x08, 0x04, 0x02, 0x81, 0x40, 0x9c, 0xf8, 0xf5, 0x8a,
|
||||
0xc4, 0x5f, 0xc3, 0xe3, 0x3d, 0xf2, 0x88, 0x76, 0xdf, 0xf9, 0x0f, 0x90, 0x50, 0x65, 0x65, 0xe5,
|
||||
0x8f, 0xa8, 0x88, 0xac, 0xf2, 0x3d, 0xcd, 0xc8, 0xf1, 0x89, 0x88, 0xcc, 0xca, 0xc8, 0xc8, 0xc8,
|
||||
0xac, 0x1f, 0x1d, 0xdd, 0xcd, 0x2f, 0xb6, 0xf3, 0x42, 0x48, 0x51, 0x6e, 0x97, 0xbc, 0x58, 0x24,
|
||||
0x31, 0x6f, 0xfe, 0x1d, 0xaa, 0x3f, 0x0f, 0xde, 0x67, 0xd9, 0x52, 0x2e, 0x73, 0xfe, 0xc9, 0xc7,
|
||||
0x96, 0x8c, 0xc5, 0x6c, 0xc6, 0xb2, 0x49, 0x59, 0x23, 0x9f, 0x7c, 0x64, 0x25, 0x7c, 0xc1, 0x33,
|
||||
0xa9, 0xff, 0xfe, 0xec, 0x7f, 0xff, 0x67, 0x25, 0xfa, 0x60, 0x37, 0x4d, 0x78, 0x26, 0x77, 0xb5,
|
||||
0xc6, 0xe0, 0xeb, 0xe8, 0xbb, 0x3b, 0x79, 0x7e, 0xc0, 0xe5, 0x1b, 0x5e, 0x94, 0x89, 0xc8, 0x06,
|
||||
0x9f, 0x0e, 0xb5, 0x83, 0xe1, 0x59, 0x1e, 0x0f, 0x77, 0xf2, 0x7c, 0x68, 0x85, 0xc3, 0x33, 0xfe,
|
||||
0xd3, 0x39, 0x2f, 0xe5, 0x27, 0x0f, 0xc3, 0x50, 0x99, 0x8b, 0xac, 0xe4, 0x83, 0xcb, 0xe8, 0x37,
|
||||
0x76, 0xf2, 0x7c, 0xc4, 0xe5, 0x1e, 0xaf, 0x3a, 0x30, 0x92, 0x4c, 0xf2, 0xc1, 0x5a, 0x4b, 0xd5,
|
||||
0x07, 0x8c, 0x8f, 0xf5, 0x6e, 0x50, 0xfb, 0x19, 0x47, 0xdf, 0xa9, 0xfc, 0x5c, 0xcd, 0xe5, 0x44,
|
||||
0xdc, 0x64, 0x83, 0xfb, 0x6d, 0x45, 0x2d, 0x32, 0xb6, 0x1f, 0x84, 0x10, 0x6d, 0xf5, 0x6d, 0xf4,
|
||||
0xab, 0x6f, 0x59, 0x9a, 0x72, 0xb9, 0x5b, 0xf0, 0xaa, 0xe1, 0xbe, 0x4e, 0x2d, 0x1a, 0xd6, 0x32,
|
||||
0x63, 0xf7, 0xd3, 0x20, 0xa3, 0x0d, 0x7f, 0x1d, 0x7d, 0xb7, 0x96, 0x9c, 0xf1, 0x58, 0x2c, 0x78,
|
||||
0x31, 0x40, 0xb5, 0xb4, 0x90, 0xb8, 0xe4, 0x2d, 0x08, 0xda, 0xde, 0x15, 0xd9, 0x82, 0x17, 0x12,
|
||||
0xb7, 0xad, 0x85, 0x61, 0xdb, 0x16, 0xd2, 0xb6, 0xff, 0x76, 0x25, 0xfa, 0xfe, 0x4e, 0x1c, 0x8b,
|
||||
0x79, 0x26, 0x8f, 0x44, 0xcc, 0xd2, 0xa3, 0x24, 0xbb, 0x3e, 0xe1, 0x37, 0xbb, 0x57, 0x15, 0x9f,
|
||||
0x4d, 0xf9, 0xe0, 0xb9, 0x7f, 0x55, 0x6b, 0x74, 0x68, 0xd8, 0xa1, 0x0b, 0x1b, 0xdf, 0x9f, 0xdf,
|
||||
0x4e, 0x49, 0xb7, 0xe5, 0x1f, 0x57, 0xa2, 0x3b, 0xb0, 0x2d, 0x23, 0x91, 0x2e, 0xb8, 0x6d, 0xcd,
|
||||
0x8b, 0x0e, 0xc3, 0x3e, 0x6e, 0xda, 0xf3, 0xc5, 0x6d, 0xd5, 0x74, 0x8b, 0xd2, 0xe8, 0x43, 0x37,
|
||||
0x5c, 0x46, 0xbc, 0x54, 0xd3, 0xe9, 0x31, 0x1d, 0x11, 0x1a, 0x31, 0x9e, 0x9f, 0xf4, 0x41, 0xb5,
|
||||
0xb7, 0x24, 0x1a, 0x68, 0x6f, 0xa9, 0x28, 0x8d, 0xb3, 0x75, 0xd4, 0x82, 0x43, 0x18, 0x5f, 0x8f,
|
||||
0x7b, 0x90, 0xda, 0xd5, 0x1f, 0x45, 0xbf, 0xf6, 0x56, 0x14, 0xd7, 0x65, 0xce, 0x62, 0xae, 0xa7,
|
||||
0xc2, 0x23, 0x5f, 0xbb, 0x91, 0xc2, 0xd9, 0xb0, 0xda, 0x85, 0x39, 0x41, 0xdb, 0x08, 0x5f, 0xe7,
|
||||
0x1c, 0xe6, 0x20, 0xab, 0x58, 0x09, 0xa9, 0xa0, 0x85, 0x90, 0xb6, 0x7d, 0x1d, 0x0d, 0xac, 0xed,
|
||||
0x8b, 0x3f, 0xe6, 0xb1, 0xdc, 0x99, 0x4c, 0xe0, 0xa8, 0x58, 0x5d, 0x45, 0x0c, 0x77, 0x26, 0x13,
|
||||
0x6a, 0x54, 0x70, 0x54, 0x3b, 0xbb, 0x89, 0x3e, 0x02, 0xce, 0x8e, 0x92, 0x52, 0x39, 0xdc, 0x0a,
|
||||
0x5b, 0xd1, 0x98, 0x71, 0x3a, 0xec, 0x8b, 0x6b, 0xc7, 0x7f, 0xbe, 0x12, 0x7d, 0x0f, 0xf1, 0x7c,
|
||||
0xc6, 0x67, 0x62, 0xc1, 0x07, 0x4f, 0xbb, 0xad, 0xd5, 0xa4, 0xf1, 0xff, 0xd9, 0x2d, 0x34, 0x90,
|
||||
0x30, 0x19, 0xf1, 0x94, 0xc7, 0x92, 0x0c, 0x93, 0x5a, 0xdc, 0x19, 0x26, 0x06, 0x73, 0x66, 0x58,
|
||||
0x23, 0x3c, 0xe0, 0x72, 0x77, 0x5e, 0x14, 0x3c, 0x93, 0xe4, 0x58, 0x5a, 0xa4, 0x73, 0x2c, 0x3d,
|
||||
0x14, 0xe9, 0xcf, 0x01, 0x97, 0x3b, 0x69, 0x4a, 0xf6, 0xa7, 0x16, 0x77, 0xf6, 0xc7, 0x60, 0xda,
|
||||
0x43, 0x1c, 0xfd, 0xba, 0x73, 0xc5, 0xe4, 0x61, 0x76, 0x29, 0x06, 0xf4, 0xb5, 0x50, 0x72, 0xe3,
|
||||
0x63, 0xad, 0x93, 0x43, 0xba, 0xf1, 0xea, 0x5d, 0x2e, 0x0a, 0x7a, 0x58, 0x6a, 0x71, 0x67, 0x37,
|
||||
0x0c, 0xa6, 0x3d, 0xfc, 0x61, 0xf4, 0x81, 0xce, 0x92, 0xcd, 0x7a, 0xf6, 0x10, 0x4d, 0xa1, 0x70,
|
||||
0x41, 0x7b, 0xd4, 0x41, 0xd9, 0xe4, 0xa0, 0x65, 0x3a, 0xf9, 0x7c, 0x8a, 0xea, 0x81, 0xd4, 0xf3,
|
||||
0x30, 0x0c, 0xb5, 0x6c, 0xef, 0xf1, 0x94, 0x93, 0xb6, 0x6b, 0x61, 0x87, 0x6d, 0x03, 0x69, 0xdb,
|
||||
0x45, 0xf4, 0x5b, 0xe6, 0xb2, 0x54, 0xeb, 0xa8, 0x92, 0x57, 0x49, 0x7a, 0x83, 0xe8, 0xb7, 0x0b,
|
||||
0x19, 0x5f, 0x9b, 0xfd, 0xe0, 0x56, 0x7f, 0xf4, 0x0c, 0xc4, 0xfb, 0x03, 0xe6, 0xdf, 0xc3, 0x30,
|
||||
0xa4, 0x6d, 0xff, 0xdd, 0x4a, 0xf4, 0x03, 0x2d, 0x7b, 0x95, 0xb1, 0x8b, 0x94, 0xab, 0x25, 0xf1,
|
||||
0x84, 0xcb, 0x1b, 0x51, 0x5c, 0x8f, 0x96, 0x59, 0x4c, 0x2c, 0xff, 0x38, 0xdc, 0xb1, 0xfc, 0x93,
|
||||
0x4a, 0x4e, 0xc5, 0xa7, 0x3b, 0x2a, 0x45, 0x0e, 0x2b, 0xbe, 0xa6, 0x07, 0x52, 0xe4, 0x54, 0xc5,
|
||||
0xe7, 0x23, 0x2d, 0xab, 0xc7, 0x55, 0xda, 0xc4, 0xad, 0x1e, 0xbb, 0x79, 0xf2, 0x41, 0x08, 0xb1,
|
||||
0x69, 0xab, 0x09, 0x60, 0x91, 0x5d, 0x26, 0xd3, 0xf3, 0x7c, 0x52, 0x85, 0xf1, 0x63, 0x3c, 0x42,
|
||||
0x1d, 0x84, 0x48, 0x5b, 0x04, 0xaa, 0xbd, 0xfd, 0x83, 0x2d, 0x8c, 0xf4, 0x54, 0xda, 0x2f, 0xc4,
|
||||
0xec, 0x88, 0x4f, 0x59, 0xbc, 0xd4, 0xf3, 0xff, 0xf3, 0xd0, 0xc4, 0x83, 0xb4, 0x69, 0xc4, 0x8b,
|
||||
0x5b, 0x6a, 0xe9, 0xf6, 0xfc, 0xfb, 0x4a, 0xf4, 0xb0, 0xe9, 0xfe, 0x15, 0xcb, 0xa6, 0x5c, 0x8f,
|
||||
0x67, 0xdd, 0xfa, 0x9d, 0x6c, 0x72, 0xc6, 0x4b, 0xc9, 0x0a, 0x39, 0xf8, 0x11, 0xde, 0xc9, 0x90,
|
||||
0x8e, 0x69, 0xdb, 0x8f, 0x7f, 0x29, 0x5d, 0x3b, 0xea, 0xa3, 0x2a, 0xb1, 0xe9, 0x14, 0xe0, 0x8f,
|
||||
0xba, 0x92, 0xc0, 0x04, 0xf0, 0x20, 0x84, 0xd8, 0x51, 0x57, 0x82, 0xc3, 0x6c, 0x91, 0x48, 0x7e,
|
||||
0xc0, 0x33, 0x5e, 0xb4, 0x47, 0xbd, 0x56, 0xf5, 0x11, 0x62, 0xd4, 0x09, 0xd4, 0x26, 0x1b, 0xcf,
|
||||
0x9b, 0x59, 0x1c, 0x37, 0x02, 0x46, 0x5a, 0xcb, 0xe3, 0x66, 0x3f, 0xd8, 0xee, 0xee, 0x1c, 0x9f,
|
||||
0x67, 0x7c, 0x21, 0xae, 0xe1, 0xee, 0xce, 0x35, 0x51, 0x03, 0xc4, 0xee, 0x0e, 0x05, 0xed, 0x0a,
|
||||
0xe6, 0xf8, 0x79, 0x93, 0xf0, 0x1b, 0xb0, 0x82, 0xb9, 0xca, 0x95, 0x98, 0x58, 0xc1, 0x10, 0x4c,
|
||||
0x7b, 0x38, 0x89, 0x7e, 0x45, 0x09, 0x7f, 0x5f, 0x24, 0xd9, 0xe0, 0x2e, 0xa2, 0x54, 0x09, 0x8c,
|
||||
0xd5, 0x7b, 0x34, 0x00, 0x5a, 0x5c, 0xfd, 0x75, 0x97, 0x65, 0x31, 0x4f, 0xd1, 0x16, 0x5b, 0x71,
|
||||
0xb0, 0xc5, 0x1e, 0x66, 0x4b, 0x07, 0x25, 0xac, 0xf2, 0xd7, 0xe8, 0x8a, 0x15, 0x49, 0x36, 0x1d,
|
||||
0x60, 0xba, 0x8e, 0x9c, 0x28, 0x1d, 0x30, 0x0e, 0x84, 0xb0, 0x56, 0xdc, 0xc9, 0xf3, 0xa2, 0x4a,
|
||||
0x8b, 0x58, 0x08, 0xfb, 0x48, 0x30, 0x84, 0x5b, 0x28, 0xee, 0x6d, 0x8f, 0xc7, 0x69, 0x92, 0x05,
|
||||
0xbd, 0x69, 0xa4, 0x8f, 0x37, 0x8b, 0x82, 0xe0, 0x3d, 0xe2, 0x6c, 0xc1, 0x9b, 0x9e, 0x61, 0x57,
|
||||
0xc6, 0x05, 0x82, 0xc1, 0x0b, 0x40, 0xbb, 0x4f, 0x53, 0xe2, 0x63, 0x76, 0xcd, 0xab, 0x0b, 0xcc,
|
||||
0xab, 0x75, 0x6d, 0x80, 0xe9, 0x7b, 0x04, 0xb1, 0x4f, 0xc3, 0x49, 0xed, 0x6a, 0x1e, 0x7d, 0xa4,
|
||||
0xe4, 0xa7, 0xac, 0x90, 0x49, 0x9c, 0xe4, 0x2c, 0x6b, 0xea, 0x7f, 0x6c, 0x5e, 0xb7, 0x28, 0xe3,
|
||||
0x72, 0xab, 0x27, 0xad, 0xdd, 0xfe, 0xdb, 0x4a, 0x74, 0x1f, 0xfa, 0x3d, 0xe5, 0xc5, 0x2c, 0x51,
|
||||
0xdb, 0xc8, 0xb2, 0x4e, 0xc2, 0x83, 0x2f, 0xc3, 0x46, 0x5b, 0x0a, 0xa6, 0x35, 0x3f, 0xbc, 0xbd,
|
||||
0xa2, 0x2d, 0x86, 0x46, 0xba, 0xb4, 0x7e, 0x5d, 0x4c, 0x5a, 0xc7, 0x2c, 0xa3, 0xa6, 0x5e, 0x56,
|
||||
0x42, 0xa2, 0x18, 0x6a, 0x41, 0x60, 0x86, 0x9f, 0x67, 0x65, 0x63, 0x1d, 0x9b, 0xe1, 0x56, 0x1c,
|
||||
0x9c, 0xe1, 0x1e, 0x66, 0x67, 0xf8, 0xe9, 0xfc, 0x22, 0x4d, 0xca, 0xab, 0x24, 0x9b, 0xea, 0xca,
|
||||
0xd7, 0xd7, 0xb5, 0x62, 0x58, 0xfc, 0xae, 0x75, 0x72, 0x98, 0x13, 0x1d, 0x2c, 0xa4, 0x13, 0x10,
|
||||
0x26, 0x6b, 0x9d, 0x9c, 0xdd, 0x1f, 0x58, 0x69, 0xb5, 0x73, 0x04, 0xfb, 0x03, 0x47, 0xb5, 0x92,
|
||||
0x12, 0xfb, 0x83, 0x36, 0xa5, 0xcd, 0x8b, 0xe8, 0x37, 0xdd, 0x3e, 0x94, 0x22, 0x5d, 0xf0, 0xf3,
|
||||
0x22, 0x19, 0x3c, 0xa1, 0xdb, 0xd7, 0x30, 0xc6, 0xd5, 0x46, 0x2f, 0xd6, 0x26, 0x2a, 0x4b, 0x1c,
|
||||
0x70, 0x39, 0x92, 0x4c, 0xce, 0x4b, 0x90, 0xa8, 0x1c, 0x1b, 0x06, 0x21, 0x12, 0x15, 0x81, 0x6a,
|
||||
0x6f, 0x7f, 0x10, 0x45, 0xf5, 0xa6, 0x5b, 0x1d, 0x8c, 0xf8, 0x6b, 0x8f, 0xde, 0x8d, 0x7b, 0xa7,
|
||||
0x22, 0xf7, 0x03, 0x84, 0x2d, 0x78, 0xea, 0xbf, 0xab, 0xf3, 0x9e, 0x01, 0xaa, 0xa1, 0x44, 0x44,
|
||||
0xc1, 0x03, 0x10, 0xd8, 0xd0, 0xd1, 0x95, 0xb8, 0xc1, 0x1b, 0x5a, 0x49, 0xc2, 0x0d, 0xd5, 0x84,
|
||||
0x3d, 0x81, 0xd5, 0x0d, 0xc5, 0x4e, 0x60, 0x9b, 0x66, 0x84, 0x4e, 0x60, 0x21, 0x63, 0x63, 0xc6,
|
||||
0x35, 0xfc, 0x52, 0x88, 0xeb, 0x19, 0x2b, 0xae, 0x41, 0xcc, 0x78, 0xca, 0x0d, 0x43, 0xc4, 0x0c,
|
||||
0xc5, 0xda, 0x98, 0x71, 0x1d, 0x56, 0xe5, 0xf2, 0x79, 0x91, 0x82, 0x98, 0xf1, 0x6c, 0x68, 0x84,
|
||||
0x88, 0x19, 0x02, 0xb5, 0xd9, 0xc9, 0xf5, 0x36, 0xe2, 0x70, 0xcf, 0xef, 0xa9, 0x8f, 0x38, 0xb5,
|
||||
0xe7, 0x47, 0x30, 0x18, 0x42, 0x07, 0x05, 0xcb, 0xaf, 0xf0, 0x10, 0x52, 0xa2, 0x70, 0x08, 0x35,
|
||||
0x08, 0x1c, 0xef, 0x11, 0x67, 0x45, 0x7c, 0x85, 0x8f, 0x77, 0x2d, 0x0b, 0x8f, 0xb7, 0x61, 0xe0,
|
||||
0x78, 0xd7, 0x82, 0xb7, 0x89, 0xbc, 0x3a, 0xe6, 0x92, 0xe1, 0xe3, 0xed, 0x33, 0xe1, 0xf1, 0x6e,
|
||||
0xb1, 0xb6, 0x1e, 0x77, 0x1d, 0x8e, 0xe6, 0x17, 0x65, 0x5c, 0x24, 0x17, 0x7c, 0x10, 0xb0, 0x62,
|
||||
0x20, 0xa2, 0x1e, 0x27, 0x61, 0xed, 0xf3, 0xe7, 0x2b, 0xd1, 0xdd, 0x66, 0xd8, 0x45, 0x59, 0xea,
|
||||
0xb5, 0xcf, 0x77, 0xff, 0x02, 0x1f, 0x5f, 0x02, 0x27, 0xce, 0xc4, 0x7b, 0xa8, 0x39, 0xb5, 0x01,
|
||||
0xde, 0xa4, 0xf3, 0xac, 0x34, 0x8d, 0xfa, 0xb2, 0x8f, 0x75, 0x47, 0x81, 0xa8, 0x0d, 0x7a, 0x29,
|
||||
0xda, 0xb2, 0x4c, 0x8f, 0x4f, 0x23, 0x3b, 0x9c, 0x94, 0xa0, 0x2c, 0x6b, 0xae, 0xb7, 0x43, 0x10,
|
||||
0x65, 0x19, 0x4e, 0xc2, 0x50, 0x38, 0x28, 0xc4, 0x3c, 0x2f, 0x3b, 0x42, 0x01, 0x40, 0xe1, 0x50,
|
||||
0x68, 0xc3, 0xda, 0xe7, 0xbb, 0xe8, 0xb7, 0xdd, 0xf0, 0x73, 0x2f, 0xf6, 0x16, 0x1d, 0x53, 0xd8,
|
||||
0x25, 0x1e, 0xf6, 0xc5, 0x6d, 0x45, 0xd1, 0x78, 0x96, 0x7b, 0x5c, 0xb2, 0x24, 0x2d, 0x07, 0xab,
|
||||
0xb8, 0x8d, 0x46, 0x4e, 0x54, 0x14, 0x18, 0x07, 0xf3, 0xdb, 0xde, 0x3c, 0x4f, 0x93, 0xb8, 0x7d,
|
||||
0x47, 0x42, 0xeb, 0x1a, 0x71, 0x38, 0xbf, 0xb9, 0x18, 0xcc, 0xd7, 0x55, 0xe9, 0xa7, 0xfe, 0x33,
|
||||
0x5e, 0xe6, 0x1c, 0xcf, 0xd7, 0x1e, 0x12, 0xce, 0xd7, 0x10, 0x85, 0xfd, 0x19, 0x71, 0x79, 0xc4,
|
||||
0x96, 0x62, 0x4e, 0xe4, 0x6b, 0x23, 0x0e, 0xf7, 0xc7, 0xc5, 0xec, 0xde, 0xc0, 0x78, 0x38, 0xcc,
|
||||
0x24, 0x2f, 0x32, 0x96, 0xee, 0xa7, 0x6c, 0x5a, 0x0e, 0x88, 0x1c, 0xe3, 0x53, 0xc4, 0xde, 0x80,
|
||||
0xa6, 0x91, 0xcb, 0x78, 0x58, 0xee, 0xb3, 0x85, 0x28, 0x12, 0x49, 0x5f, 0x46, 0x8b, 0x74, 0x5e,
|
||||
0x46, 0x0f, 0x45, 0xbd, 0xed, 0x14, 0xf1, 0x55, 0xb2, 0xe0, 0x93, 0x80, 0xb7, 0x06, 0xe9, 0xe1,
|
||||
0xcd, 0x41, 0x91, 0x41, 0x1b, 0x89, 0x79, 0x11, 0x73, 0x72, 0xd0, 0x6a, 0x71, 0xe7, 0xa0, 0x19,
|
||||
0x4c, 0x7b, 0xf8, 0xab, 0x95, 0xe8, 0x77, 0x6a, 0xa9, 0x7b, 0x9b, 0x60, 0x8f, 0x95, 0x57, 0x17,
|
||||
0x82, 0x15, 0x93, 0xc1, 0x67, 0x98, 0x1d, 0x14, 0x35, 0xae, 0x9f, 0xdd, 0x46, 0x05, 0x5e, 0xd6,
|
||||
0xaa, 0xee, 0xb6, 0x33, 0x0e, 0xbd, 0xac, 0x1e, 0x12, 0xbe, 0xac, 0x10, 0x85, 0x09, 0x44, 0xc9,
|
||||
0xeb, 0x23, 0xb9, 0x55, 0x52, 0xdf, 0x3f, 0x97, 0x5b, 0xeb, 0xe4, 0x60, 0x7e, 0xac, 0x84, 0x7e,
|
||||
0xb4, 0x6c, 0x51, 0x36, 0xf0, 0x88, 0x19, 0xf6, 0xc5, 0x49, 0xcf, 0x66, 0x56, 0x84, 0x3d, 0xb7,
|
||||
0x66, 0xc6, 0xb0, 0x2f, 0x4e, 0x78, 0x76, 0xd2, 0x5a, 0xc8, 0x33, 0x92, 0xda, 0x86, 0x7d, 0x71,
|
||||
0x58, 0x7d, 0x69, 0xa6, 0x59, 0x17, 0x9e, 0x04, 0xec, 0xc0, 0xb5, 0x61, 0xa3, 0x17, 0xab, 0x1d,
|
||||
0xfe, 0xcd, 0x4a, 0xf4, 0x7d, 0xeb, 0xf1, 0x58, 0x4c, 0x92, 0xcb, 0x65, 0x0d, 0xbd, 0x61, 0xe9,
|
||||
0x9c, 0x97, 0x83, 0x67, 0x94, 0xb5, 0x36, 0x6b, 0x5a, 0xf0, 0xfc, 0x56, 0x3a, 0x70, 0xee, 0xec,
|
||||
0xe4, 0x79, 0xba, 0x1c, 0xf3, 0x59, 0x9e, 0x92, 0x73, 0xc7, 0x43, 0xc2, 0x73, 0x07, 0xa2, 0xb0,
|
||||
0x2a, 0x1f, 0x8b, 0xaa, 0xe6, 0x47, 0xab, 0x72, 0x25, 0x0a, 0x57, 0xe5, 0x0d, 0x02, 0x6b, 0xa5,
|
||||
0xb1, 0xd8, 0x15, 0x69, 0xca, 0x63, 0xd9, 0x7e, 0xd4, 0xc0, 0x68, 0x5a, 0x22, 0x5c, 0x2b, 0x01,
|
||||
0xd2, 0x9e, 0xca, 0x35, 0x7b, 0x48, 0x56, 0xf0, 0x97, 0xcb, 0xa3, 0x24, 0xbb, 0x1e, 0xe0, 0x65,
|
||||
0x81, 0x05, 0x88, 0x53, 0x39, 0x14, 0x84, 0x7b, 0xd5, 0xf3, 0x6c, 0x22, 0xf0, 0xbd, 0x6a, 0x25,
|
||||
0x09, 0xef, 0x55, 0x35, 0x01, 0x4d, 0x9e, 0x71, 0xca, 0x64, 0x25, 0x09, 0x9b, 0xd4, 0x04, 0x96,
|
||||
0x0a, 0xf5, 0xbd, 0x1b, 0x32, 0x15, 0x82, 0xbb, 0x35, 0x6b, 0x9d, 0x1c, 0x8c, 0xd0, 0x66, 0xd3,
|
||||
0xba, 0xcf, 0x65, 0x7c, 0x85, 0x47, 0xa8, 0x87, 0x84, 0x23, 0x14, 0xa2, 0xb0, 0x4b, 0x63, 0x61,
|
||||
0x36, 0xdd, 0xab, 0x78, 0x7c, 0xb4, 0x36, 0xdc, 0x6b, 0x9d, 0x1c, 0xdc, 0x46, 0x1e, 0xce, 0xd4,
|
||||
0x35, 0x43, 0x83, 0xbc, 0x96, 0x85, 0xb7, 0x91, 0x86, 0x81, 0xad, 0xaf, 0x05, 0xea, 0x2c, 0x6b,
|
||||
0x95, 0x56, 0xf4, 0x4e, 0xb3, 0xd6, 0x3a, 0x39, 0xed, 0xe4, 0x5f, 0xcc, 0x36, 0xae, 0x96, 0x9e,
|
||||
0x88, 0x6a, 0x8e, 0xbc, 0x61, 0x69, 0x32, 0x61, 0x92, 0x8f, 0xc5, 0x35, 0xcf, 0xf0, 0x1d, 0x93,
|
||||
0x6e, 0x6d, 0xcd, 0x0f, 0x3d, 0x85, 0xf0, 0x8e, 0x29, 0xac, 0x08, 0xe3, 0xa4, 0xa6, 0xcf, 0x4b,
|
||||
0xbe, 0xcb, 0x4a, 0x22, 0x93, 0x79, 0x48, 0x38, 0x4e, 0x20, 0x0a, 0xeb, 0xd5, 0x5a, 0xfe, 0xea,
|
||||
0x5d, 0xce, 0x8b, 0x84, 0x67, 0x31, 0xc7, 0xeb, 0x55, 0x48, 0x85, 0xeb, 0x55, 0x84, 0x86, 0x7b,
|
||||
0xb5, 0x3d, 0x26, 0xf9, 0xcb, 0xe5, 0x38, 0x99, 0xf1, 0x52, 0xb2, 0x59, 0x8e, 0xef, 0xd5, 0x00,
|
||||
0x14, 0xde, 0xab, 0xb5, 0xe1, 0xd6, 0xd1, 0x90, 0x49, 0x88, 0xed, 0x27, 0x94, 0x20, 0x11, 0x78,
|
||||
0x42, 0x89, 0x40, 0xe1, 0x85, 0xb5, 0x00, 0x7a, 0x93, 0xa0, 0x65, 0x25, 0x78, 0x93, 0x80, 0xa6,
|
||||
0x5b, 0x07, 0x6e, 0x86, 0x19, 0x55, 0x53, 0xb3, 0xa3, 0xe9, 0x23, 0x77, 0x8a, 0x6e, 0xf4, 0x62,
|
||||
0xf1, 0x13, 0xbe, 0x33, 0x9e, 0x32, 0xb5, 0x6c, 0x05, 0x8e, 0xd1, 0x1a, 0xa6, 0xcf, 0x09, 0x9f,
|
||||
0xc3, 0x6a, 0x87, 0x7f, 0xb1, 0x12, 0x7d, 0x82, 0x79, 0x7c, 0x9d, 0x2b, 0xbf, 0x4f, 0xbb, 0x6d,
|
||||
0xd5, 0x24, 0xf1, 0x08, 0x56, 0x58, 0x43, 0xb7, 0xe1, 0x4f, 0xa2, 0x8f, 0x1b, 0x91, 0x7d, 0x42,
|
||||
0x4b, 0x37, 0xc0, 0x2f, 0xda, 0x4c, 0xfb, 0x21, 0x67, 0xdc, 0x6f, 0xf7, 0xe6, 0xed, 0x7e, 0xc8,
|
||||
0x6f, 0x57, 0x09, 0xf6, 0x43, 0xc6, 0x86, 0x16, 0x13, 0xfb, 0x21, 0x04, 0xb3, 0xb3, 0xd3, 0xed,
|
||||
0xde, 0xdb, 0x44, 0x5e, 0xa9, 0x7a, 0x0b, 0xcc, 0x4e, 0xaf, 0xad, 0x06, 0x22, 0x66, 0x27, 0x09,
|
||||
0xc3, 0x8a, 0xa4, 0x01, 0xab, 0xb9, 0x89, 0xe5, 0x72, 0x63, 0xc8, 0x9d, 0x99, 0xeb, 0xdd, 0x20,
|
||||
0x8c, 0xd7, 0x46, 0xac, 0xb7, 0x3e, 0x4f, 0x42, 0x16, 0xc0, 0xf6, 0x67, 0xa3, 0x17, 0xab, 0x1d,
|
||||
0xfe, 0x59, 0xf4, 0xbd, 0x56, 0xc7, 0xf6, 0x39, 0x93, 0xf3, 0x82, 0x4f, 0x06, 0xdb, 0x1d, 0xed,
|
||||
0x6e, 0x40, 0xe3, 0xfa, 0x69, 0x7f, 0x85, 0x56, 0x8d, 0xde, 0x70, 0x75, 0x58, 0x99, 0x36, 0x3c,
|
||||
0x0b, 0x99, 0xf4, 0xd9, 0x60, 0x8d, 0x4e, 0xeb, 0xb4, 0xb6, 0xd9, 0x6e, 0x74, 0xed, 0x2c, 0x58,
|
||||
0x92, 0xaa, 0x9b, 0xb5, 0x9f, 0x85, 0x8c, 0x7a, 0x68, 0x70, 0x9b, 0x4d, 0xaa, 0xb4, 0x32, 0xb3,
|
||||
0x9a, 0xe3, 0xce, 0xf6, 0x6c, 0x93, 0xce, 0x04, 0xc8, 0xee, 0x6c, 0xab, 0x27, 0xad, 0xdd, 0xca,
|
||||
0x66, 0xc9, 0xab, 0xfe, 0xec, 0x06, 0x39, 0xe6, 0x55, 0xab, 0x22, 0x91, 0xbe, 0xd5, 0x93, 0xd6,
|
||||
0x5e, 0xff, 0x34, 0xfa, 0xb8, 0xed, 0x55, 0x2f, 0x44, 0xdb, 0x9d, 0xa6, 0xc0, 0x5a, 0xf4, 0xb4,
|
||||
0xbf, 0x82, 0xdd, 0xd2, 0x7c, 0x95, 0x94, 0x52, 0x14, 0xcb, 0xd1, 0x95, 0xb8, 0x69, 0xde, 0x7c,
|
||||
0xf0, 0x67, 0xab, 0x06, 0x86, 0x0e, 0x41, 0x6c, 0x69, 0x70, 0xb2, 0xe5, 0xca, 0xbe, 0x21, 0x51,
|
||||
0x12, 0xae, 0x1c, 0xa2, 0xc3, 0x95, 0x4f, 0xda, 0x5c, 0xd5, 0xf4, 0xca, 0xbe, 0xce, 0xb1, 0x86,
|
||||
0x37, 0xb5, 0xfd, 0x4a, 0xc7, 0x7a, 0x37, 0x68, 0x2b, 0x16, 0x2d, 0xde, 0x4b, 0x2e, 0x2f, 0x4d,
|
||||
0x9f, 0xf0, 0x96, 0xba, 0x08, 0x51, 0xb1, 0x10, 0xa8, 0x2d, 0xba, 0xf7, 0x93, 0x94, 0xab, 0x13,
|
||||
0xfd, 0xd7, 0x97, 0x97, 0xa9, 0x60, 0x13, 0x50, 0x74, 0x57, 0xe2, 0xa1, 0x2b, 0x27, 0x8a, 0x6e,
|
||||
0x8c, 0xb3, 0xcf, 0x0a, 0x54, 0xd2, 0x33, 0x1e, 0x8b, 0x2c, 0x4e, 0x52, 0xf8, 0x20, 0xa8, 0xd2,
|
||||
0x34, 0x42, 0xe2, 0x59, 0x81, 0x16, 0x64, 0x17, 0xc6, 0x4a, 0x54, 0x4d, 0xfb, 0xa6, 0xfd, 0x8f,
|
||||
0xda, 0x8a, 0x8e, 0x98, 0x58, 0x18, 0x11, 0xcc, 0xee, 0x3d, 0x2b, 0xe1, 0x79, 0xae, 0x8c, 0xdf,
|
||||
0x6b, 0x6b, 0xd5, 0x12, 0x62, 0xef, 0xe9, 0x13, 0x76, 0x0f, 0x55, 0xfd, 0x7d, 0x4f, 0xdc, 0x64,
|
||||
0xca, 0xe8, 0x83, 0xb6, 0x4a, 0x23, 0x23, 0xf6, 0x50, 0x90, 0xd1, 0x86, 0x7f, 0x12, 0xfd, 0x7f,
|
||||
0x65, 0xb8, 0x10, 0xf9, 0xe0, 0x0e, 0xa2, 0x50, 0x38, 0xcf, 0x6c, 0xde, 0x25, 0xe5, 0xf6, 0xd1,
|
||||
0x02, 0x13, 0x1b, 0xe7, 0x25, 0x9b, 0xf2, 0xc1, 0x43, 0x62, 0xc4, 0x95, 0x94, 0x78, 0xb4, 0xa0,
|
||||
0x4d, 0xf9, 0x51, 0x71, 0x22, 0x26, 0xda, 0x3a, 0xd2, 0x43, 0x23, 0x0c, 0x45, 0x85, 0x0b, 0xd9,
|
||||
0x62, 0xe6, 0x84, 0x2d, 0x92, 0xa9, 0x59, 0x70, 0xea, 0xbc, 0x55, 0x82, 0x62, 0xc6, 0x32, 0x43,
|
||||
0x07, 0x22, 0x8a, 0x19, 0x12, 0xd6, 0x3e, 0xff, 0x79, 0x25, 0xba, 0x67, 0x99, 0x83, 0xe6, 0xb4,
|
||||
0xee, 0x30, 0xbb, 0x14, 0x55, 0xe9, 0x73, 0x94, 0x64, 0xd7, 0xe5, 0xe0, 0x0b, 0xca, 0x24, 0xce,
|
||||
0x9b, 0xa6, 0x7c, 0x79, 0x6b, 0x3d, 0x5b, 0xb5, 0x36, 0x47, 0x59, 0xf6, 0x7e, 0x76, 0xad, 0x01,
|
||||
0xaa, 0x56, 0x73, 0xe2, 0x05, 0x39, 0xa2, 0x6a, 0x0d, 0xf1, 0x76, 0x88, 0x8d, 0xf3, 0x54, 0x64,
|
||||
0x70, 0x88, 0xad, 0x85, 0x4a, 0x48, 0x0c, 0x71, 0x0b, 0xb2, 0xf9, 0xb8, 0x11, 0xd5, 0xa7, 0x2e,
|
||||
0x3b, 0x69, 0x0a, 0xf2, 0xb1, 0x51, 0x35, 0x00, 0x91, 0x8f, 0x51, 0x50, 0xfb, 0x39, 0x8b, 0xbe,
|
||||
0x53, 0x5d, 0xd2, 0xd3, 0x82, 0x2f, 0x12, 0x0e, 0x1f, 0xbd, 0x70, 0x24, 0xc4, 0xfc, 0xf7, 0x09,
|
||||
0x3b, 0xb3, 0xce, 0xb3, 0x32, 0x4f, 0x59, 0x79, 0xa5, 0x6f, 0xc6, 0xfb, 0x7d, 0x6e, 0x84, 0xf0,
|
||||
0x76, 0xfc, 0xa3, 0x0e, 0xca, 0x26, 0xf5, 0x46, 0x66, 0x52, 0xcc, 0x2a, 0xae, 0xda, 0x4a, 0x33,
|
||||
0x6b, 0x9d, 0x9c, 0x3d, 0xf1, 0x3e, 0x60, 0x69, 0xca, 0x8b, 0x65, 0x23, 0x3b, 0x66, 0x59, 0x72,
|
||||
0xc9, 0x4b, 0x09, 0x4e, 0xbc, 0x35, 0x35, 0x84, 0x18, 0x71, 0xe2, 0x1d, 0xc0, 0x6d, 0x35, 0x0f,
|
||||
0x3c, 0x1f, 0x66, 0x13, 0xfe, 0x0e, 0x54, 0xf3, 0xd0, 0x8e, 0x62, 0x88, 0x6a, 0x9e, 0x62, 0xed,
|
||||
0xc9, 0xef, 0xcb, 0x54, 0xc4, 0xd7, 0x7a, 0x09, 0xf0, 0x07, 0x58, 0x49, 0xe0, 0x1a, 0xf0, 0x20,
|
||||
0x84, 0xd8, 0x45, 0x40, 0x09, 0xce, 0x78, 0x9e, 0xb2, 0x18, 0x3e, 0x7f, 0x53, 0xeb, 0x68, 0x19,
|
||||
0xb1, 0x08, 0x40, 0x06, 0x34, 0x57, 0x3f, 0xd7, 0x83, 0x35, 0x17, 0x3c, 0xd6, 0xf3, 0x20, 0x84,
|
||||
0xd8, 0x65, 0x50, 0x09, 0x46, 0x79, 0x9a, 0x48, 0x30, 0x0d, 0x6a, 0x0d, 0x25, 0x21, 0xa6, 0x81,
|
||||
0x4f, 0x00, 0x93, 0xc7, 0xbc, 0x98, 0x72, 0xd4, 0xa4, 0x92, 0x04, 0x4d, 0x36, 0x84, 0x7d, 0xd8,
|
||||
0xb8, 0xee, 0xbb, 0xc8, 0x97, 0xe0, 0x61, 0x63, 0xdd, 0x2d, 0x91, 0x2f, 0x89, 0x87, 0x8d, 0x3d,
|
||||
0x00, 0x34, 0xf1, 0x94, 0x95, 0x12, 0x6f, 0xa2, 0x92, 0x04, 0x9b, 0xd8, 0x10, 0x76, 0x8d, 0xae,
|
||||
0x9b, 0x38, 0x97, 0x60, 0x8d, 0xd6, 0x0d, 0x70, 0xee, 0x40, 0xdf, 0x25, 0xe5, 0x36, 0x93, 0xd4,
|
||||
0xa3, 0xc2, 0xe5, 0x7e, 0xc2, 0xd3, 0x49, 0x09, 0x32, 0x89, 0xbe, 0xee, 0x8d, 0x94, 0xc8, 0x24,
|
||||
0x6d, 0x0a, 0x84, 0x92, 0x3e, 0x1f, 0xc7, 0x7a, 0x07, 0x8e, 0xc6, 0x1f, 0x84, 0x10, 0x9b, 0x9f,
|
||||
0x9a, 0x46, 0xef, 0xb2, 0xa2, 0x48, 0xaa, 0xc5, 0x7f, 0x15, 0x6f, 0x50, 0x23, 0x27, 0xf2, 0x13,
|
||||
0xc6, 0x81, 0xe9, 0xd5, 0x24, 0x6e, 0xac, 0x61, 0x30, 0x75, 0x7f, 0x1a, 0x64, 0x6c, 0xc5, 0xa9,
|
||||
0x24, 0xce, 0x2d, 0x54, 0xec, 0x6a, 0x22, 0x77, 0x50, 0x57, 0xbb, 0x30, 0xe7, 0x65, 0x20, 0xe3,
|
||||
0xe2, 0x58, 0x2c, 0xf8, 0x58, 0xbc, 0x7a, 0x97, 0x94, 0x32, 0xc9, 0xa6, 0x7a, 0xe5, 0x7e, 0x4e,
|
||||
0x58, 0xc2, 0x60, 0xe2, 0x65, 0xa0, 0x4e, 0x25, 0x5b, 0x40, 0x80, 0xb6, 0x9c, 0xf0, 0x1b, 0xb4,
|
||||
0x80, 0x80, 0x16, 0x0d, 0x47, 0x14, 0x10, 0x21, 0xde, 0x9e, 0xa3, 0x18, 0xe7, 0xfa, 0x8d, 0xe9,
|
||||
0xb1, 0x68, 0x6a, 0x39, 0xca, 0x1a, 0x04, 0x89, 0xad, 0x6c, 0x50, 0xc1, 0xee, 0x2f, 0x8d, 0x7f,
|
||||
0x3b, 0xc5, 0xd6, 0x09, 0x3b, 0xed, 0x69, 0xf6, 0xb8, 0x07, 0x89, 0xb8, 0xb2, 0xcf, 0x01, 0x50,
|
||||
0xae, 0xda, 0x8f, 0x01, 0x3c, 0xee, 0x41, 0x3a, 0x67, 0x32, 0x6e, 0xb7, 0x5e, 0xb2, 0xf8, 0x7a,
|
||||
0x5a, 0x88, 0x79, 0x36, 0xd9, 0x15, 0xa9, 0x28, 0xc0, 0x99, 0x8c, 0xd7, 0x6a, 0x80, 0x12, 0x67,
|
||||
0x32, 0x1d, 0x2a, 0xb6, 0x82, 0x73, 0x5b, 0xb1, 0x93, 0x26, 0x53, 0xb8, 0xa3, 0xf6, 0x0c, 0x29,
|
||||
0x80, 0xa8, 0xe0, 0x50, 0x10, 0x09, 0xa2, 0x7a, 0xc7, 0x2d, 0x93, 0x98, 0xa5, 0xb5, 0xbf, 0x6d,
|
||||
0xda, 0x8c, 0x07, 0x76, 0x06, 0x11, 0xa2, 0x80, 0xf4, 0x73, 0x3c, 0x2f, 0xb2, 0xc3, 0x4c, 0x0a,
|
||||
0xb2, 0x9f, 0x0d, 0xd0, 0xd9, 0x4f, 0x07, 0x04, 0x69, 0x75, 0xcc, 0xdf, 0x55, 0xad, 0xa9, 0xfe,
|
||||
0xc1, 0xd2, 0x6a, 0xf5, 0xf7, 0xa1, 0x96, 0x87, 0xd2, 0x2a, 0xe0, 0x40, 0x67, 0xb4, 0x93, 0x3a,
|
||||
0x60, 0x02, 0xda, 0x7e, 0x98, 0xac, 0x77, 0x83, 0xb8, 0x9f, 0x91, 0x5c, 0xa6, 0x3c, 0xe4, 0x47,
|
||||
0x01, 0x7d, 0xfc, 0x34, 0xa0, 0x3d, 0x6e, 0xf1, 0xfa, 0x73, 0xc5, 0xe3, 0xeb, 0xd6, 0x63, 0x4d,
|
||||
0x7e, 0x43, 0x6b, 0x84, 0x38, 0x6e, 0x21, 0x50, 0x7c, 0x88, 0x0e, 0x63, 0x91, 0x85, 0x86, 0xa8,
|
||||
0x92, 0xf7, 0x19, 0x22, 0xcd, 0xd9, 0xcd, 0xaf, 0x91, 0xea, 0xc8, 0xac, 0x87, 0x69, 0x83, 0xb0,
|
||||
0xe0, 0x42, 0xc4, 0xe6, 0x97, 0x84, 0x6d, 0x4d, 0x0e, 0x7d, 0x1e, 0xb7, 0x9f, 0xf9, 0x6e, 0x59,
|
||||
0x39, 0xa6, 0x9f, 0xf9, 0xa6, 0x58, 0xba, 0x93, 0x75, 0x8c, 0x74, 0x58, 0xf1, 0xe3, 0x64, 0xb3,
|
||||
0x1f, 0x6c, 0xb7, 0x3c, 0x9e, 0xcf, 0xdd, 0x94, 0xb3, 0xa2, 0xf6, 0xba, 0x15, 0x30, 0x64, 0x31,
|
||||
0x62, 0xcb, 0x13, 0xc0, 0x41, 0x0a, 0xf3, 0x3c, 0xef, 0x8a, 0x4c, 0xf2, 0x4c, 0x62, 0x29, 0xcc,
|
||||
0x37, 0xa6, 0xc1, 0x50, 0x0a, 0xa3, 0x14, 0x40, 0xdc, 0xaa, 0xf3, 0x20, 0x2e, 0x4f, 0xd8, 0x0c,
|
||||
0xad, 0xd8, 0xea, 0xb3, 0x9e, 0x5a, 0x1e, 0x8a, 0x5b, 0xc0, 0x39, 0x37, 0xf9, 0x5c, 0x2f, 0x63,
|
||||
0x56, 0x4c, 0xcd, 0xe9, 0xc6, 0x64, 0xf0, 0x94, 0xb6, 0xe3, 0x93, 0xc4, 0x4d, 0xbe, 0xb0, 0x06,
|
||||
0x48, 0x3b, 0x87, 0x33, 0x36, 0x35, 0x3d, 0x45, 0x7a, 0xa0, 0xe4, 0xad, 0xae, 0xae, 0x77, 0x83,
|
||||
0xc0, 0xcf, 0x9b, 0x64, 0xc2, 0x45, 0xc0, 0x8f, 0x92, 0xf7, 0xf1, 0x03, 0x41, 0x50, 0xbd, 0x55,
|
||||
0xfd, 0xae, 0x77, 0x74, 0x3b, 0xd9, 0x44, 0xef, 0x63, 0x87, 0xc4, 0xe5, 0x01, 0x5c, 0xa8, 0x7a,
|
||||
0x23, 0x78, 0x30, 0x47, 0x9b, 0x03, 0xda, 0xd0, 0x1c, 0x35, 0xe7, 0xaf, 0x7d, 0xe6, 0x28, 0x06,
|
||||
0x6b, 0x9f, 0x3f, 0xd3, 0x73, 0x74, 0x8f, 0x49, 0x56, 0xd5, 0xed, 0x6f, 0x12, 0x7e, 0xa3, 0x37,
|
||||
0xc2, 0x48, 0x7f, 0x1b, 0x6a, 0xa8, 0x5e, 0x59, 0x05, 0xbb, 0xe2, 0xed, 0xde, 0x7c, 0xc0, 0xb7,
|
||||
0xde, 0x21, 0x74, 0xfa, 0x06, 0x5b, 0x85, 0xed, 0xde, 0x7c, 0xc0, 0xb7, 0x7e, 0x17, 0xbe, 0xd3,
|
||||
0x37, 0x78, 0x21, 0x7e, 0xbb, 0x37, 0xaf, 0x7d, 0xff, 0x65, 0x33, 0x71, 0x5d, 0xe7, 0x55, 0x1d,
|
||||
0x16, 0xcb, 0x64, 0xc1, 0xb1, 0x72, 0xd2, 0xb7, 0x67, 0xd0, 0x50, 0x39, 0x49, 0xab, 0x38, 0x1f,
|
||||
0x50, 0xc2, 0x5a, 0x71, 0x2a, 0xca, 0x44, 0xdd, 0xa4, 0x7f, 0xde, 0xc3, 0x68, 0x03, 0x87, 0x36,
|
||||
0x4d, 0x21, 0x25, 0x7b, 0xbb, 0xd1, 0x43, 0xed, 0x53, 0xcc, 0x9b, 0x01, 0x7b, 0xed, 0x87, 0x99,
|
||||
0xb7, 0x7a, 0xd2, 0xf6, 0xc6, 0x9f, 0xc7, 0xb8, 0x77, 0x1c, 0x43, 0xa3, 0x8a, 0xde, 0x74, 0x7c,
|
||||
0xda, 0x5f, 0x41, 0xbb, 0xff, 0xeb, 0x66, 0x5f, 0x01, 0xfd, 0xeb, 0x49, 0xf0, 0xac, 0x8f, 0x45,
|
||||
0x30, 0x11, 0x9e, 0xdf, 0x4a, 0x47, 0x37, 0xe4, 0xef, 0x9b, 0x0d, 0x74, 0x83, 0xaa, 0x77, 0x39,
|
||||
0xd4, 0x3b, 0xa0, 0x7a, 0x4e, 0x84, 0x86, 0xd5, 0xc2, 0x70, 0x66, 0xbc, 0xb8, 0xa5, 0x96, 0xf3,
|
||||
0x39, 0x2d, 0x0f, 0xd6, 0xef, 0x1c, 0x3a, 0xed, 0x09, 0x59, 0x76, 0x68, 0xd8, 0xa0, 0x2f, 0x6e,
|
||||
0xab, 0x46, 0xcd, 0x15, 0x07, 0x56, 0x5f, 0xe7, 0x78, 0xde, 0xd3, 0xb0, 0xf7, 0xbd, 0x8e, 0xcf,
|
||||
0x6f, 0xa7, 0xa4, 0xdb, 0xf2, 0x1f, 0x2b, 0xd1, 0x23, 0x8f, 0xb5, 0xf7, 0x13, 0xc0, 0xa9, 0xc7,
|
||||
0x8f, 0x03, 0xf6, 0x29, 0x25, 0xd3, 0xb8, 0xdf, 0xfd, 0xe5, 0x94, 0xed, 0xb7, 0xa7, 0x3c, 0x95,
|
||||
0xfd, 0x24, 0x95, 0xbc, 0x68, 0x7f, 0x7b, 0xca, 0xb7, 0x5b, 0x53, 0x43, 0xfa, 0xdb, 0x53, 0x01,
|
||||
0xdc, 0xf9, 0xf6, 0x14, 0xe2, 0x19, 0xfd, 0xf6, 0x14, 0x6a, 0x2d, 0xf8, 0xed, 0xa9, 0xb0, 0x06,
|
||||
0x95, 0xde, 0x9b, 0x26, 0xd4, 0xe7, 0xd6, 0xbd, 0x2c, 0xfa, 0xc7, 0xd8, 0xcf, 0x6e, 0xa3, 0x42,
|
||||
0x2c, 0x70, 0x35, 0xa7, 0x9e, 0x73, 0xeb, 0x71, 0x4d, 0xbd, 0x67, 0xdd, 0xb6, 0x7b, 0xf3, 0xda,
|
||||
0xf7, 0x4f, 0xf5, 0xee, 0xc6, 0xa4, 0x73, 0x51, 0xa8, 0xef, 0x8e, 0x6d, 0x84, 0xd2, 0x73, 0x65,
|
||||
0xc1, 0x1d, 0xf9, 0xcd, 0x7e, 0x30, 0xd1, 0xdd, 0x8a, 0xd0, 0x83, 0x3e, 0xec, 0x32, 0x04, 0x86,
|
||||
0x7c, 0xbb, 0x37, 0x4f, 0x2c, 0x23, 0xb5, 0xef, 0x7a, 0xb4, 0x7b, 0x18, 0xf3, 0xc7, 0xfa, 0x69,
|
||||
0x7f, 0x05, 0xed, 0x7e, 0xa1, 0xcb, 0x46, 0xd7, 0xbd, 0x1a, 0xe7, 0xad, 0x2e, 0x53, 0x23, 0x6f,
|
||||
0x98, 0x87, 0x7d, 0xf1, 0x50, 0x01, 0xe1, 0x2e, 0xa1, 0x5d, 0x05, 0x04, 0xba, 0x8c, 0x7e, 0x7e,
|
||||
0x3b, 0x25, 0xdd, 0x96, 0x7f, 0x5a, 0x89, 0xee, 0x92, 0x6d, 0xd1, 0x71, 0xf0, 0x45, 0x5f, 0xcb,
|
||||
0x20, 0x1e, 0xbe, 0xbc, 0xb5, 0x9e, 0x6e, 0xd4, 0xbf, 0xae, 0x44, 0xf7, 0x02, 0x8d, 0xaa, 0x03,
|
||||
0xe4, 0x16, 0xd6, 0xfd, 0x40, 0xf9, 0xe1, 0xed, 0x15, 0xa9, 0xe5, 0xde, 0xc5, 0x47, 0xed, 0x8f,
|
||||
0x32, 0x05, 0x6c, 0x8f, 0xe8, 0x8f, 0x32, 0x75, 0x6b, 0xc1, 0x43, 0x1e, 0x76, 0xd1, 0x6c, 0xba,
|
||||
0xd0, 0x43, 0x1e, 0xf5, 0x84, 0x5a, 0xf0, 0xe3, 0x12, 0x18, 0x87, 0x39, 0x79, 0xf5, 0x2e, 0x67,
|
||||
0xd9, 0x84, 0x76, 0x52, 0xcb, 0xbb, 0x9d, 0x18, 0x0e, 0x1e, 0x8e, 0x55, 0xd2, 0x33, 0xd1, 0x6c,
|
||||
0xa4, 0x1e, 0x53, 0xfa, 0x06, 0x09, 0x1e, 0x8e, 0xb5, 0x50, 0xc2, 0x9b, 0xae, 0x1a, 0x43, 0xde,
|
||||
0x40, 0xb1, 0xf8, 0xa4, 0x0f, 0x0a, 0x4a, 0x74, 0xe3, 0xcd, 0x9c, 0xb9, 0x6f, 0x86, 0xac, 0xb4,
|
||||
0xce, 0xdd, 0xb7, 0x7a, 0xd2, 0x84, 0xdb, 0x11, 0x97, 0x5f, 0x71, 0x36, 0xe1, 0x45, 0xd0, 0xad,
|
||||
0xa1, 0x7a, 0xb9, 0x75, 0x69, 0xcc, 0xed, 0xae, 0x48, 0xe7, 0xb3, 0x4c, 0x0f, 0x26, 0xe9, 0xd6,
|
||||
0xa5, 0xba, 0xdd, 0x02, 0x1a, 0x1e, 0x0b, 0x5a, 0xb7, 0xaa, 0xbc, 0x7c, 0x12, 0x36, 0xe3, 0x55,
|
||||
0x95, 0x1b, 0xbd, 0x58, 0xba, 0x9f, 0x3a, 0x8c, 0x3a, 0xfa, 0x09, 0x22, 0x69, 0xab, 0x27, 0x0d,
|
||||
0xcf, 0xe7, 0x1c, 0xb7, 0x26, 0x9e, 0xb6, 0x3b, 0x6c, 0xb5, 0x42, 0xea, 0x69, 0x7f, 0x05, 0x78,
|
||||
0x1a, 0xaa, 0xa3, 0xea, 0x28, 0x29, 0xe5, 0x7e, 0x92, 0xa6, 0x83, 0x8d, 0x40, 0x98, 0x34, 0x50,
|
||||
0xf0, 0x34, 0x14, 0x81, 0x89, 0x48, 0x6e, 0x4e, 0x0f, 0xb3, 0x41, 0x97, 0x1d, 0x45, 0xf5, 0x8a,
|
||||
0x64, 0x97, 0x06, 0x27, 0x5a, 0xce, 0xa5, 0x36, 0xbd, 0x1d, 0x86, 0x2f, 0x5c, 0xab, 0xc3, 0xdb,
|
||||
0xbd, 0x79, 0x70, 0xbb, 0x5d, 0x51, 0x6a, 0x65, 0x79, 0x48, 0x99, 0xf0, 0x56, 0x92, 0x47, 0x1d,
|
||||
0x14, 0x38, 0x15, 0xac, 0xa7, 0xd1, 0xdb, 0x64, 0x32, 0xe5, 0x12, 0xbd, 0x53, 0xe4, 0x02, 0xc1,
|
||||
0x3b, 0x45, 0x00, 0x04, 0x43, 0x57, 0xff, 0xdd, 0x1c, 0x87, 0x1e, 0x4e, 0xb0, 0xa1, 0xd3, 0xca,
|
||||
0x0e, 0x15, 0x1a, 0x3a, 0x94, 0x06, 0xd9, 0xc0, 0xb8, 0xd5, 0xaf, 0xe3, 0x3f, 0x09, 0x99, 0x01,
|
||||
0xef, 0xe4, 0x6f, 0xf4, 0x62, 0xc1, 0x8a, 0x62, 0x1d, 0x26, 0xb3, 0x44, 0x62, 0x2b, 0x8a, 0x63,
|
||||
0xa3, 0x42, 0x42, 0x2b, 0x4a, 0x1b, 0xa5, 0xba, 0x57, 0xd5, 0x08, 0x87, 0x93, 0x70, 0xf7, 0x6a,
|
||||
0xa6, 0x5f, 0xf7, 0x0c, 0xdb, 0xba, 0xb1, 0x99, 0x99, 0x90, 0x91, 0x57, 0x7a, 0xb3, 0x8c, 0xc4,
|
||||
0xb6, 0x7a, 0x4d, 0x13, 0x82, 0xa1, 0xac, 0x43, 0x29, 0xc0, 0x03, 0xfb, 0x8a, 0x6b, 0xee, 0xbd,
|
||||
0xe6, 0x39, 0x67, 0x05, 0xcb, 0x62, 0x74, 0x73, 0xaa, 0x0c, 0xb6, 0xc8, 0xd0, 0xe6, 0x94, 0xd4,
|
||||
0x00, 0xb7, 0xcd, 0xfd, 0x17, 0x2c, 0x91, 0xa9, 0x60, 0xde, 0x64, 0xf4, 0xdf, 0xaf, 0x7c, 0xdc,
|
||||
0x83, 0x84, 0xb7, 0xcd, 0x1b, 0xc0, 0x1c, 0x7c, 0xd7, 0x4e, 0x3f, 0x0b, 0x98, 0xf2, 0xd1, 0xd0,
|
||||
0x46, 0x98, 0x56, 0x01, 0x41, 0x6d, 0x0a, 0x5c, 0x2e, 0x7f, 0xc2, 0x97, 0x58, 0x50, 0xdb, 0xfa,
|
||||
0x54, 0x21, 0xa1, 0xa0, 0x6e, 0xa3, 0xa0, 0xce, 0x74, 0xf7, 0x41, 0xab, 0x01, 0x7d, 0x77, 0xeb,
|
||||
0xb3, 0xd6, 0xc9, 0x81, 0x99, 0xb3, 0x97, 0x2c, 0xbc, 0xfb, 0x04, 0x48, 0x43, 0xf7, 0x92, 0x05,
|
||||
0x7e, 0x9b, 0x60, 0xa3, 0x17, 0x0b, 0x6f, 0xc9, 0x33, 0xc9, 0xdf, 0x35, 0xf7, 0xca, 0x91, 0xe6,
|
||||
0x2a, 0x79, 0xeb, 0x66, 0xf9, 0x7a, 0x37, 0x68, 0x1f, 0x80, 0x3d, 0x2d, 0x44, 0xcc, 0xcb, 0x52,
|
||||
0x7f, 0xa9, 0xd2, 0x7f, 0xc2, 0x48, 0xcb, 0x86, 0xe0, 0x3b, 0x95, 0x0f, 0xc3, 0x90, 0xf3, 0x79,
|
||||
0xb9, 0x5a, 0x64, 0xbf, 0x7a, 0xb3, 0x8a, 0x6a, 0xb6, 0x3f, 0x78, 0xb3, 0xd6, 0xc9, 0xd9, 0xe9,
|
||||
0xa5, 0xa5, 0xee, 0x67, 0x6e, 0xd6, 0x51, 0x75, 0xec, 0x0b, 0x37, 0x8f, 0x7b, 0x90, 0xda, 0xd5,
|
||||
0x57, 0xd1, 0xfb, 0x47, 0x62, 0x3a, 0xe2, 0xd9, 0x64, 0xf0, 0x03, 0xff, 0x11, 0x5a, 0x31, 0x1d,
|
||||
0x56, 0x7f, 0x36, 0x46, 0xef, 0x50, 0x62, 0xfb, 0x10, 0xe0, 0x1e, 0xbf, 0x98, 0x4f, 0x47, 0x92,
|
||||
0x49, 0xf0, 0x10, 0xa0, 0xfa, 0xfb, 0xb0, 0x12, 0x10, 0x0f, 0x01, 0x7a, 0x00, 0xb0, 0x37, 0x2e,
|
||||
0x38, 0x47, 0xed, 0x55, 0x82, 0xa0, 0x3d, 0x0d, 0xd8, 0x2a, 0xc2, 0xd8, 0xab, 0x0a, 0x75, 0xf8,
|
||||
0xd0, 0x9e, 0xd5, 0x51, 0x52, 0xa2, 0x8a, 0x68, 0x53, 0x36, 0xb8, 0xeb, 0xee, 0xab, 0xaf, 0x8e,
|
||||
0xcc, 0x67, 0x33, 0x56, 0x2c, 0x41, 0x70, 0xeb, 0x5e, 0x3a, 0x00, 0x11, 0xdc, 0x28, 0x68, 0x67,
|
||||
0x6d, 0x73, 0x99, 0xe3, 0xeb, 0x03, 0x51, 0x88, 0xb9, 0x4c, 0x32, 0x0e, 0xbf, 0x3c, 0x61, 0x2e,
|
||||
0xa8, 0xcb, 0x10, 0xb3, 0x96, 0x62, 0x6d, 0x95, 0xab, 0x88, 0xfa, 0x79, 0x42, 0xf5, 0xfd, 0xea,
|
||||
0x52, 0x8a, 0x02, 0xde, 0x4f, 0xac, 0xad, 0x40, 0x88, 0xa8, 0x72, 0x49, 0x18, 0x8c, 0xfd, 0x69,
|
||||
0x92, 0x4d, 0xd1, 0xb1, 0x3f, 0x75, 0xbf, 0xfe, 0x7a, 0x8f, 0x06, 0xec, 0x84, 0xaa, 0x2f, 0x5a,
|
||||
0x3d, 0x01, 0xf4, 0xbb, 0x9c, 0xe8, 0x45, 0x77, 0x09, 0x62, 0x42, 0xe1, 0x24, 0x70, 0xf5, 0x3a,
|
||||
0xe7, 0x19, 0x9f, 0x34, 0x4f, 0xcd, 0x61, 0xae, 0x3c, 0x22, 0xe8, 0x0a, 0x92, 0x36, 0x17, 0x29,
|
||||
0xf9, 0xd9, 0x3c, 0x3b, 0x2d, 0xc4, 0x65, 0x92, 0xf2, 0x02, 0xe4, 0xa2, 0x5a, 0xdd, 0x91, 0x13,
|
||||
0xb9, 0x08, 0xe3, 0xec, 0xe3, 0x17, 0x4a, 0xea, 0x7d, 0x84, 0x7d, 0x5c, 0xb0, 0x18, 0x3e, 0x7e,
|
||||
0x51, 0xdb, 0x68, 0x63, 0xc4, 0xc9, 0x60, 0x00, 0x77, 0x0a, 0x9d, 0xda, 0x75, 0xb6, 0x54, 0xf1,
|
||||
0xa1, 0xdf, 0x25, 0x54, 0xdf, 0x44, 0x2d, 0x41, 0xa1, 0xa3, 0xcd, 0x61, 0x24, 0x51, 0xe8, 0x84,
|
||||
0x35, 0xec, 0x52, 0xa2, 0xb8, 0x13, 0xfd, 0x58, 0x11, 0x58, 0x4a, 0x6a, 0x1b, 0x8d, 0x90, 0x58,
|
||||
0x4a, 0x5a, 0x10, 0x48, 0x48, 0xcd, 0x34, 0x98, 0xa2, 0x09, 0xc9, 0x48, 0x83, 0x09, 0xc9, 0xa5,
|
||||
0x6c, 0xa2, 0x38, 0xcc, 0x12, 0x99, 0xb0, 0x74, 0xc4, 0xe5, 0x29, 0x2b, 0xd8, 0x8c, 0x4b, 0x5e,
|
||||
0xc0, 0x44, 0xa1, 0x91, 0xa1, 0xc7, 0x10, 0x89, 0x82, 0x62, 0xb5, 0xc3, 0xdf, 0x8b, 0x3e, 0xac,
|
||||
0xd6, 0x7d, 0x9e, 0xe9, 0x9f, 0x5b, 0x79, 0xa5, 0x7e, 0xa7, 0x69, 0xf0, 0x91, 0xb1, 0x31, 0x92,
|
||||
0x05, 0x67, 0xb3, 0xc6, 0xf6, 0x07, 0xe6, 0xef, 0x0a, 0x7c, 0xba, 0x52, 0xc5, 0xf3, 0x89, 0x90,
|
||||
0xc9, 0x65, 0xb5, 0xcd, 0xd6, 0x6f, 0x10, 0x81, 0x78, 0x76, 0xc5, 0xc3, 0xc0, 0xb7, 0x28, 0x30,
|
||||
0xce, 0xe6, 0x69, 0x57, 0x7a, 0xc6, 0xf3, 0x14, 0xe6, 0x69, 0x4f, 0x5b, 0x01, 0x44, 0x9e, 0x46,
|
||||
0x41, 0x3b, 0x39, 0x5d, 0xf1, 0x98, 0x87, 0x3b, 0x33, 0xe6, 0xfd, 0x3a, 0x33, 0xf6, 0x5e, 0xca,
|
||||
0x48, 0xa3, 0x0f, 0x8f, 0xf9, 0xec, 0x82, 0x17, 0xe5, 0x55, 0x92, 0x53, 0xdf, 0x6d, 0xb5, 0x44,
|
||||
0xe7, 0x77, 0x5b, 0x09, 0xd4, 0xae, 0x04, 0x16, 0x38, 0x2c, 0x4f, 0xd8, 0x8c, 0xab, 0x2f, 0x6b,
|
||||
0x80, 0x95, 0xc0, 0x31, 0xe2, 0x40, 0xc4, 0x4a, 0x40, 0xc2, 0xce, 0xfb, 0x5d, 0x96, 0x39, 0xe3,
|
||||
0xd3, 0x2a, 0xc2, 0x8a, 0x53, 0xb6, 0x9c, 0xf1, 0x4c, 0x6a, 0x93, 0xe0, 0x4c, 0xde, 0x31, 0x89,
|
||||
0xf3, 0xc4, 0x99, 0x7c, 0x1f, 0x3d, 0x27, 0x35, 0x79, 0x17, 0xfe, 0x54, 0x14, 0xb2, 0xfe, 0x31,
|
||||
0xa5, 0xf3, 0x22, 0x05, 0xa9, 0xc9, 0xbf, 0xa8, 0x1e, 0x49, 0xa4, 0xa6, 0xb0, 0x86, 0xf3, 0x2b,
|
||||
0x04, 0x5e, 0x1b, 0xde, 0xf0, 0xc2, 0xc4, 0xc9, 0xab, 0x19, 0x4b, 0x52, 0x1d, 0x0d, 0x3f, 0x0a,
|
||||
0xd8, 0x26, 0x74, 0x88, 0x5f, 0x21, 0xe8, 0xab, 0xeb, 0xfc, 0x6e, 0x43, 0xb8, 0x85, 0xe0, 0x16,
|
||||
0x41, 0x87, 0x7d, 0xe2, 0x16, 0x41, 0xb7, 0x96, 0xdd, 0xb9, 0x5b, 0x56, 0x71, 0x4b, 0x45, 0xec,
|
||||
0x8a, 0x09, 0x3c, 0x2f, 0x74, 0x6c, 0x02, 0x90, 0xd8, 0xb9, 0x07, 0x15, 0x6c, 0x69, 0x60, 0xb1,
|
||||
0xfd, 0x24, 0x63, 0x69, 0xf2, 0x33, 0x58, 0xd6, 0x3b, 0x76, 0x1a, 0x82, 0x28, 0x0d, 0x70, 0x12,
|
||||
0x73, 0x75, 0xc0, 0xe5, 0x38, 0xa9, 0x52, 0xff, 0x7a, 0xe0, 0xba, 0x29, 0xa2, 0xdb, 0x95, 0x43,
|
||||
0x3a, 0xdf, 0x68, 0x85, 0x97, 0x75, 0x27, 0xcf, 0x47, 0xd5, 0xaa, 0x7a, 0xc6, 0x63, 0x9e, 0xe4,
|
||||
0x72, 0xf0, 0x22, 0x7c, 0xad, 0x00, 0x4e, 0x3c, 0x68, 0xd1, 0x43, 0xcd, 0xb9, 0x7d, 0x5f, 0xe5,
|
||||
0x92, 0x51, 0xfd, 0x2b, 0x83, 0xe7, 0x25, 0x2f, 0x74, 0xa1, 0x71, 0xc0, 0x25, 0x98, 0x9d, 0x0e,
|
||||
0x37, 0x74, 0xc0, 0xaa, 0xa3, 0xc4, 0xec, 0x0c, 0x6b, 0xd8, 0xc3, 0x3e, 0x87, 0xd3, 0xdf, 0xdc,
|
||||
0x56, 0xcf, 0x1b, 0x6e, 0x92, 0xc6, 0x1c, 0x8a, 0x38, 0xec, 0xa3, 0x69, 0x5b, 0xad, 0xb5, 0xdd,
|
||||
0xee, 0x64, 0xcb, 0x43, 0xf8, 0xc8, 0x04, 0x62, 0x49, 0x61, 0x44, 0xb5, 0x16, 0xc0, 0x9d, 0xc3,
|
||||
0xf0, 0x42, 0xb0, 0x49, 0xcc, 0x4a, 0x79, 0xca, 0x96, 0xa9, 0x60, 0x13, 0xb5, 0xae, 0xc3, 0xc3,
|
||||
0xf0, 0x86, 0x19, 0xba, 0x10, 0x75, 0x18, 0x4e, 0xc1, 0x6e, 0x75, 0xa6, 0x7e, 0x3c, 0x51, 0x3f,
|
||||
0xcb, 0x09, 0xab, 0x33, 0xd5, 0x5e, 0xf8, 0x1c, 0xe7, 0xc3, 0x30, 0x64, 0xdf, 0x41, 0xab, 0x45,
|
||||
0xaa, 0x0c, 0xb9, 0x87, 0xe9, 0x78, 0x05, 0xc8, 0xfd, 0x00, 0x61, 0xbf, 0x4b, 0x51, 0xff, 0xbd,
|
||||
0xf9, 0xfd, 0x1f, 0xa9, 0xbf, 0x64, 0xbd, 0x89, 0xe9, 0xba, 0xd0, 0xd0, 0xfd, 0xc0, 0xdd, 0x56,
|
||||
0x4f, 0xda, 0x96, 0x99, 0xbb, 0x57, 0x4c, 0xee, 0x4c, 0x26, 0xc7, 0xbc, 0x44, 0x5e, 0x28, 0xaf,
|
||||
0x84, 0x43, 0x2b, 0x25, 0xca, 0xcc, 0x36, 0x65, 0x03, 0xbd, 0x92, 0xbd, 0x9a, 0x24, 0x52, 0xcb,
|
||||
0x9a, 0x27, 0xa4, 0x37, 0xdb, 0x06, 0xda, 0x14, 0xd1, 0x2b, 0x9a, 0xb6, 0xb9, 0xbc, 0x62, 0xc6,
|
||||
0x62, 0x3a, 0x4d, 0xb9, 0x86, 0xce, 0x38, 0xab, 0x3f, 0xe4, 0xb7, 0xdd, 0xb6, 0x85, 0x82, 0x44,
|
||||
0x2e, 0x0f, 0x2a, 0xd8, 0x32, 0xb2, 0xc2, 0xea, 0x5b, 0x52, 0xcd, 0x85, 0x5d, 0x6b, 0x9b, 0xf1,
|
||||
0x00, 0xa2, 0x8c, 0x44, 0x41, 0xfb, 0xde, 0x5b, 0x25, 0x3e, 0xe0, 0xcd, 0x95, 0x80, 0x9f, 0x20,
|
||||
0x52, 0xca, 0x8e, 0x98, 0x78, 0xef, 0x0d, 0xc1, 0xec, 0x3e, 0x01, 0x78, 0x78, 0xb9, 0x3c, 0x9c,
|
||||
0xc0, 0x7d, 0x02, 0xd4, 0x57, 0x0c, 0xb1, 0x4f, 0xa0, 0x58, 0x7f, 0xe8, 0xcc, 0xb9, 0xd7, 0x11,
|
||||
0x2b, 0x6d, 0xe7, 0x90, 0xa1, 0x43, 0xc1, 0xd0, 0xd0, 0x51, 0x0a, 0xfe, 0x25, 0x75, 0x8f, 0xd6,
|
||||
0x90, 0x4b, 0x8a, 0x9d, 0xab, 0xad, 0x76, 0x61, 0x4e, 0xe1, 0xe3, 0x75, 0x71, 0x2c, 0x74, 0x33,
|
||||
0xf4, 0x7b, 0x8d, 0x25, 0x28, 0x7c, 0xfc, 0x66, 0xb7, 0x68, 0xa2, 0xf0, 0xe9, 0xd6, 0xb2, 0x79,
|
||||
0xd2, 0xec, 0x6f, 0xd5, 0x23, 0x54, 0xf8, 0x2f, 0x0a, 0xd4, 0x42, 0x22, 0x4f, 0xb6, 0xa0, 0xda,
|
||||
0xf6, 0xcb, 0xfb, 0xff, 0xf9, 0xcd, 0x9d, 0x95, 0x5f, 0x7c, 0x73, 0x67, 0xe5, 0xbf, 0xbf, 0xb9,
|
||||
0xb3, 0xf2, 0xf3, 0x6f, 0xef, 0xbc, 0xf7, 0x8b, 0x6f, 0xef, 0xbc, 0xf7, 0x5f, 0xdf, 0xde, 0x79,
|
||||
0xef, 0xeb, 0xf7, 0xf5, 0x8f, 0xfc, 0x5e, 0xfc, 0x3f, 0xf5, 0x53, 0xbd, 0xcf, 0xff, 0x2f, 0x00,
|
||||
0x00, 0xff, 0xff, 0x42, 0x38, 0x0e, 0xf7, 0x08, 0x78, 0x00, 0x00,
|
||||
// 5445 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x9d, 0xdd, 0x6f, 0x24, 0x49,
|
||||
0x52, 0xc0, 0xcf, 0x2f, 0x2c, 0xd4, 0x71, 0x0b, 0xf4, 0xc2, 0xb2, 0xb7, 0xdc, 0xcd, 0xcc, 0xce,
|
||||
0xce, 0xd8, 0x33, 0x63, 0xbb, 0x3d, 0x3b, 0xb3, 0x1f, 0xa7, 0x3b, 0x24, 0xd4, 0x63, 0x8f, 0xbd,
|
||||
0xbe, 0xb3, 0x3d, 0xc6, 0xdd, 0x9e, 0x91, 0x56, 0x42, 0xa2, 0xdc, 0x95, 0x6e, 0x17, 0xae, 0xae,
|
||||
0xac, 0xab, 0xca, 0x6e, 0x4f, 0x1f, 0x02, 0x81, 0x40, 0x20, 0x10, 0x88, 0x13, 0x5f, 0xaf, 0x48,
|
||||
0xfc, 0x35, 0x88, 0xa7, 0x7b, 0xe4, 0x11, 0xed, 0xfe, 0x05, 0xfc, 0x07, 0xa8, 0x32, 0xb3, 0xf2,
|
||||
0x23, 0x2a, 0x22, 0xab, 0x7c, 0x4f, 0x33, 0x72, 0xfc, 0x22, 0x22, 0x3f, 0x23, 0x23, 0xb3, 0xb2,
|
||||
0xaa, 0xa3, 0xbb, 0xc5, 0xc5, 0x4e, 0x51, 0x72, 0xc1, 0xab, 0x9d, 0x8a, 0x95, 0xcb, 0x74, 0xca,
|
||||
0x9a, 0x7f, 0x87, 0xf2, 0xcf, 0x83, 0x77, 0xe2, 0x7c, 0x25, 0x56, 0x05, 0xfb, 0xf0, 0x03, 0x4b,
|
||||
0x4e, 0xf9, 0x7c, 0x1e, 0xe7, 0x49, 0xa5, 0x90, 0x0f, 0xdf, 0xb7, 0x12, 0xb6, 0x64, 0xb9, 0xd0,
|
||||
0x7f, 0x7f, 0xf6, 0xdf, 0xff, 0xb7, 0x16, 0xbd, 0xbb, 0x9b, 0xa5, 0x2c, 0x17, 0xbb, 0x5a, 0x63,
|
||||
0xf0, 0x55, 0xf4, 0x9d, 0x51, 0x51, 0x1c, 0x30, 0xf1, 0x9a, 0x95, 0x55, 0xca, 0xf3, 0xc1, 0xc7,
|
||||
0x43, 0xed, 0x60, 0x78, 0x56, 0x4c, 0x87, 0xa3, 0xa2, 0x18, 0x5a, 0xe1, 0xf0, 0x8c, 0xfd, 0x74,
|
||||
0xc1, 0x2a, 0xf1, 0xe1, 0x83, 0x30, 0x54, 0x15, 0x3c, 0xaf, 0xd8, 0xe0, 0x32, 0xfa, 0xad, 0x51,
|
||||
0x51, 0x8c, 0x99, 0xd8, 0x63, 0x75, 0x05, 0xc6, 0x22, 0x16, 0x6c, 0xb0, 0xd1, 0x52, 0xf5, 0x01,
|
||||
0xe3, 0xe3, 0x51, 0x37, 0xa8, 0xfd, 0x4c, 0xa2, 0x6f, 0xd7, 0x7e, 0xae, 0x16, 0x22, 0xe1, 0x37,
|
||||
0xf9, 0xe0, 0xa3, 0xb6, 0xa2, 0x16, 0x19, 0xdb, 0xf7, 0x43, 0x88, 0xb6, 0xfa, 0x26, 0xfa, 0xf5,
|
||||
0x37, 0x71, 0x96, 0x31, 0xb1, 0x5b, 0xb2, 0xba, 0xe0, 0xbe, 0x8e, 0x12, 0x0d, 0x95, 0xcc, 0xd8,
|
||||
0xfd, 0x38, 0xc8, 0x68, 0xc3, 0x5f, 0x45, 0xdf, 0x51, 0x92, 0x33, 0x36, 0xe5, 0x4b, 0x56, 0x0e,
|
||||
0x50, 0x2d, 0x2d, 0x24, 0x9a, 0xbc, 0x05, 0x41, 0xdb, 0xbb, 0x3c, 0x5f, 0xb2, 0x52, 0xe0, 0xb6,
|
||||
0xb5, 0x30, 0x6c, 0xdb, 0x42, 0xda, 0xf6, 0xdf, 0xad, 0x45, 0xdf, 0x1b, 0x4d, 0xa7, 0x7c, 0x91,
|
||||
0x8b, 0x23, 0x3e, 0x8d, 0xb3, 0xa3, 0x34, 0xbf, 0x3e, 0x61, 0x37, 0xbb, 0x57, 0x35, 0x9f, 0xcf,
|
||||
0xd8, 0xe0, 0xb9, 0xdf, 0xaa, 0x0a, 0x1d, 0x1a, 0x76, 0xe8, 0xc2, 0xc6, 0xf7, 0xa7, 0xb7, 0x53,
|
||||
0xd2, 0x65, 0xf9, 0xa7, 0xb5, 0xe8, 0x0e, 0x2c, 0xcb, 0x98, 0x67, 0x4b, 0x66, 0x4b, 0xf3, 0x59,
|
||||
0x87, 0x61, 0x1f, 0x37, 0xe5, 0xf9, 0xfc, 0xb6, 0x6a, 0xba, 0x44, 0x59, 0xf4, 0x9e, 0x3b, 0x5c,
|
||||
0xc6, 0xac, 0x92, 0xd3, 0xe9, 0x31, 0x3d, 0x22, 0x34, 0x62, 0x3c, 0x3f, 0xe9, 0x83, 0x6a, 0x6f,
|
||||
0x69, 0x34, 0xd0, 0xde, 0x32, 0x5e, 0x19, 0x67, 0x8f, 0x50, 0x0b, 0x0e, 0x61, 0x7c, 0x3d, 0xee,
|
||||
0x41, 0x6a, 0x57, 0x7f, 0x1c, 0xfd, 0xc6, 0x1b, 0x5e, 0x5e, 0x57, 0x45, 0x3c, 0x65, 0x7a, 0x2a,
|
||||
0x3c, 0xf4, 0xb5, 0x1b, 0x29, 0x9c, 0x0d, 0xeb, 0x5d, 0x98, 0x33, 0x68, 0x1b, 0xe1, 0xab, 0x82,
|
||||
0xc1, 0x18, 0x64, 0x15, 0x6b, 0x21, 0x35, 0x68, 0x21, 0xa4, 0x6d, 0x5f, 0x47, 0x03, 0x6b, 0xfb,
|
||||
0xe2, 0x4f, 0xd8, 0x54, 0x8c, 0x92, 0x04, 0xf6, 0x8a, 0xd5, 0x95, 0xc4, 0x70, 0x94, 0x24, 0x54,
|
||||
0xaf, 0xe0, 0xa8, 0x76, 0x76, 0x13, 0xbd, 0x0f, 0x9c, 0x1d, 0xa5, 0x95, 0x74, 0xb8, 0x1d, 0xb6,
|
||||
0xa2, 0x31, 0xe3, 0x74, 0xd8, 0x17, 0xd7, 0x8e, 0xff, 0x62, 0x2d, 0xfa, 0x2e, 0xe2, 0xf9, 0x8c,
|
||||
0xcd, 0xf9, 0x92, 0x0d, 0x9e, 0x76, 0x5b, 0x53, 0xa4, 0xf1, 0xff, 0xc9, 0x2d, 0x34, 0x90, 0x61,
|
||||
0x32, 0x66, 0x19, 0x9b, 0x0a, 0x72, 0x98, 0x28, 0x71, 0xe7, 0x30, 0x31, 0x98, 0x33, 0xc3, 0x1a,
|
||||
0xe1, 0x01, 0x13, 0xbb, 0x8b, 0xb2, 0x64, 0xb9, 0x20, 0xfb, 0xd2, 0x22, 0x9d, 0x7d, 0xe9, 0xa1,
|
||||
0x48, 0x7d, 0x0e, 0x98, 0x18, 0x65, 0x19, 0x59, 0x1f, 0x25, 0xee, 0xac, 0x8f, 0xc1, 0xb4, 0x87,
|
||||
0x69, 0xf4, 0x9b, 0x4e, 0x8b, 0x89, 0xc3, 0xfc, 0x92, 0x0f, 0xe8, 0xb6, 0x90, 0x72, 0xe3, 0x63,
|
||||
0xa3, 0x93, 0x43, 0xaa, 0xf1, 0xf2, 0x6d, 0xc1, 0x4b, 0xba, 0x5b, 0x94, 0xb8, 0xb3, 0x1a, 0x06,
|
||||
0xd3, 0x1e, 0xfe, 0x28, 0x7a, 0x57, 0x47, 0xc9, 0x66, 0x3d, 0x7b, 0x80, 0x86, 0x50, 0xb8, 0xa0,
|
||||
0x3d, 0xec, 0xa0, 0x5a, 0xe6, 0x8f, 0xd3, 0x59, 0x59, 0x47, 0x1f, 0xdc, 0xbc, 0x96, 0x76, 0x98,
|
||||
0xb7, 0x94, 0x36, 0xcf, 0xa3, 0xdf, 0xf6, 0xcd, 0xef, 0xc6, 0xf9, 0x94, 0x65, 0x83, 0x27, 0x21,
|
||||
0x75, 0xc5, 0x18, 0x57, 0x9b, 0xbd, 0x58, 0x1b, 0xec, 0x34, 0xa1, 0x83, 0xe9, 0xc7, 0xa8, 0x36,
|
||||
0x08, 0xa5, 0x0f, 0xc2, 0x50, 0xcb, 0xf6, 0x1e, 0xcb, 0x18, 0x69, 0x5b, 0x09, 0x3b, 0x6c, 0x1b,
|
||||
0x48, 0xdb, 0x2e, 0xa3, 0xdf, 0x31, 0xdd, 0x5c, 0xe7, 0x05, 0x52, 0x5e, 0x2f, 0x3a, 0x9b, 0x44,
|
||||
0x3f, 0xba, 0x90, 0xf1, 0xb5, 0xd5, 0x0f, 0x6e, 0xd5, 0x47, 0x47, 0x14, 0xbc, 0x3e, 0x20, 0x9e,
|
||||
0x3c, 0x08, 0x43, 0xda, 0xf6, 0xdf, 0xaf, 0x45, 0xdf, 0xd7, 0xb2, 0x97, 0x79, 0x7c, 0x91, 0x31,
|
||||
0xb9, 0xc4, 0x9f, 0x30, 0x71, 0xc3, 0xcb, 0xeb, 0xf1, 0x2a, 0x9f, 0x12, 0xe9, 0x0c, 0x0e, 0x77,
|
||||
0xa4, 0x33, 0xa4, 0x92, 0x2e, 0xcc, 0x9f, 0x46, 0x1f, 0x34, 0x83, 0xe2, 0x2a, 0xce, 0x67, 0xec,
|
||||
0xc7, 0x15, 0xcf, 0x47, 0x45, 0x3a, 0x4a, 0x92, 0x72, 0x30, 0xc4, 0xbb, 0x1e, 0x72, 0xa6, 0x04,
|
||||
0x3b, 0xbd, 0x79, 0x27, 0x7d, 0xd6, 0xad, 0x2c, 0x78, 0x01, 0xd3, 0xe7, 0xa6, 0xf9, 0x04, 0x2f,
|
||||
0xa8, 0xf4, 0xd9, 0x47, 0x5a, 0x56, 0x8f, 0xeb, 0x35, 0x08, 0xb7, 0x7a, 0xec, 0x2e, 0x3a, 0xf7,
|
||||
0x43, 0x88, 0x5d, 0x03, 0x9a, 0x86, 0xe2, 0xf9, 0x65, 0x3a, 0x3b, 0x2f, 0x92, 0x7a, 0x0e, 0x3d,
|
||||
0xc6, 0xeb, 0xec, 0x20, 0xc4, 0x1a, 0x40, 0xa0, 0xda, 0xdb, 0x3f, 0xda, 0x2c, 0x53, 0xc7, 0xa5,
|
||||
0xfd, 0x92, 0xcf, 0x8f, 0xd8, 0x2c, 0x9e, 0xae, 0x74, 0x30, 0xfd, 0x34, 0x14, 0xc5, 0x20, 0x6d,
|
||||
0x0a, 0xf1, 0xd9, 0x2d, 0xb5, 0x74, 0x79, 0xfe, 0x63, 0x2d, 0x7a, 0xe0, 0x8d, 0x13, 0x3d, 0x98,
|
||||
0x54, 0xe9, 0x47, 0x79, 0x72, 0xc6, 0x2a, 0x11, 0x97, 0x62, 0xf0, 0xc3, 0xc0, 0x18, 0x20, 0x74,
|
||||
0x4c, 0xd9, 0x7e, 0xf4, 0x4b, 0xe9, 0xda, 0x5e, 0x1f, 0xd7, 0xab, 0x84, 0x8e, 0x3f, 0x7e, 0xaf,
|
||||
0x4b, 0x09, 0x8c, 0x3e, 0xf7, 0x43, 0x88, 0xed, 0x75, 0x29, 0x38, 0xcc, 0x97, 0xa9, 0x60, 0x07,
|
||||
0x2c, 0x67, 0x65, 0xbb, 0xd7, 0x95, 0xaa, 0x8f, 0x10, 0xbd, 0x4e, 0xa0, 0x36, 0xd2, 0x79, 0xde,
|
||||
0x4c, 0xa6, 0xb1, 0x19, 0x30, 0xd2, 0xca, 0x35, 0xb6, 0xfa, 0xc1, 0x76, 0xab, 0xec, 0xf8, 0x3c,
|
||||
0x63, 0x4b, 0x7e, 0x0d, 0xb7, 0xca, 0xae, 0x09, 0x05, 0x10, 0x5b, 0x65, 0x14, 0xb4, 0xe9, 0x80,
|
||||
0xe3, 0xe7, 0x75, 0xca, 0x6e, 0x40, 0x3a, 0xe0, 0x2a, 0xd7, 0x62, 0x22, 0x1d, 0x40, 0x30, 0xed,
|
||||
0xe1, 0x24, 0xfa, 0x35, 0x29, 0xfc, 0x31, 0x4f, 0xf3, 0xc1, 0x5d, 0x44, 0xa9, 0x16, 0x18, 0xab,
|
||||
0xf7, 0x68, 0x00, 0x94, 0xb8, 0xfe, 0xab, 0x5e, 0x9b, 0x1f, 0x12, 0x4a, 0x60, 0x59, 0x5e, 0xef,
|
||||
0xc2, 0x6c, 0x1e, 0x26, 0x85, 0x75, 0xfc, 0x1a, 0x5f, 0xc5, 0x65, 0x9a, 0xcf, 0x06, 0x98, 0xae,
|
||||
0x23, 0x27, 0xf2, 0x30, 0x8c, 0x03, 0x43, 0x58, 0x2b, 0x8e, 0x8a, 0xa2, 0xac, 0xc3, 0x22, 0x36,
|
||||
0x84, 0x7d, 0x24, 0x38, 0x84, 0x5b, 0x28, 0xee, 0x6d, 0x8f, 0x4d, 0xb3, 0x34, 0x0f, 0x7a, 0xd3,
|
||||
0x48, 0x1f, 0x6f, 0x16, 0x05, 0x83, 0xf7, 0x88, 0xc5, 0x4b, 0xd6, 0xd4, 0x0c, 0x6b, 0x19, 0x17,
|
||||
0x08, 0x0e, 0x5e, 0x00, 0xda, 0x4d, 0xaf, 0x14, 0x1f, 0xc7, 0xd7, 0xac, 0x6e, 0x60, 0x56, 0x2f,
|
||||
0xaa, 0x03, 0x4c, 0xdf, 0x23, 0x88, 0x4d, 0x2f, 0x4e, 0x6a, 0x57, 0x8b, 0xe8, 0x7d, 0x29, 0x3f,
|
||||
0x8d, 0x4b, 0x91, 0x4e, 0xd3, 0x22, 0xce, 0x9b, 0xcd, 0x14, 0x36, 0xaf, 0x5b, 0x94, 0x71, 0xb9,
|
||||
0xdd, 0x93, 0xd6, 0x6e, 0xff, 0x7d, 0x2d, 0xfa, 0x08, 0xfa, 0x3d, 0x65, 0xe5, 0x3c, 0x95, 0x7b,
|
||||
0xf2, 0x4a, 0x05, 0xe1, 0xc1, 0x17, 0x61, 0xa3, 0x2d, 0x05, 0x53, 0x9a, 0x1f, 0xdc, 0x5e, 0xd1,
|
||||
0x66, 0x62, 0x63, 0xbd, 0x4f, 0x79, 0x55, 0x26, 0xad, 0x33, 0xab, 0x71, 0xb3, 0xf9, 0x90, 0x42,
|
||||
0x22, 0x13, 0x6b, 0x41, 0x60, 0x86, 0x9f, 0xe7, 0x55, 0x63, 0x1d, 0x9b, 0xe1, 0x56, 0x1c, 0x9c,
|
||||
0xe1, 0x1e, 0x66, 0x67, 0xf8, 0xe9, 0xe2, 0x22, 0x4b, 0xab, 0xab, 0x34, 0x9f, 0xe9, 0xb4, 0xdb,
|
||||
0xd7, 0xb5, 0x62, 0x98, 0x79, 0x6f, 0x74, 0x72, 0x98, 0x13, 0x3d, 0x58, 0x48, 0x27, 0x60, 0x98,
|
||||
0x6c, 0x74, 0x72, 0x76, 0x37, 0x64, 0xa5, 0xf5, 0x36, 0x1c, 0xec, 0x86, 0x1c, 0xd5, 0x5a, 0x4a,
|
||||
0xec, 0x86, 0xda, 0x94, 0xdd, 0x0d, 0xb9, 0x75, 0xa8, 0x78, 0xb6, 0x64, 0xe7, 0x65, 0x0a, 0x76,
|
||||
0x43, 0x5e, 0xf9, 0x1a, 0x86, 0xd8, 0x0d, 0x51, 0xac, 0x0d, 0x54, 0x96, 0x38, 0x60, 0x62, 0x2c,
|
||||
0x62, 0xb1, 0xa8, 0x40, 0xa0, 0x72, 0x6c, 0x18, 0x84, 0x08, 0x54, 0x04, 0xaa, 0xbd, 0xfd, 0x61,
|
||||
0x14, 0xa9, 0x13, 0x0c, 0x79, 0xca, 0xe4, 0xaf, 0x3d, 0xfa, 0x68, 0xc3, 0x3b, 0x62, 0xfa, 0x28,
|
||||
0x40, 0xd8, 0x84, 0x47, 0xfd, 0x5d, 0x1e, 0x9e, 0x0d, 0x50, 0x0d, 0x29, 0x22, 0x12, 0x1e, 0x80,
|
||||
0xc0, 0x82, 0x8e, 0xaf, 0xf8, 0x0d, 0x5e, 0xd0, 0x5a, 0x12, 0x2e, 0xa8, 0x26, 0xec, 0x71, 0xb6,
|
||||
0x2e, 0x28, 0x76, 0x9c, 0xdd, 0x14, 0x23, 0x74, 0x9c, 0x0d, 0x19, 0x3b, 0x66, 0x5c, 0xc3, 0x2f,
|
||||
0x38, 0xbf, 0x9e, 0xc7, 0xe5, 0x35, 0x18, 0x33, 0x9e, 0x72, 0xc3, 0x10, 0x63, 0x86, 0x62, 0xed,
|
||||
0x98, 0x71, 0x1d, 0xd6, 0xe9, 0xf2, 0x79, 0x99, 0x81, 0x31, 0xe3, 0xd9, 0xd0, 0x08, 0x31, 0x66,
|
||||
0x08, 0xd4, 0x46, 0x27, 0xd7, 0xdb, 0x98, 0xc1, 0x03, 0x14, 0x4f, 0x7d, 0xcc, 0xa8, 0x03, 0x14,
|
||||
0x04, 0x83, 0x43, 0xe8, 0xa0, 0x8c, 0x8b, 0x2b, 0x7c, 0x08, 0x49, 0x51, 0x78, 0x08, 0x35, 0x08,
|
||||
0xec, 0xef, 0x31, 0x8b, 0xcb, 0xe9, 0x15, 0xde, 0xdf, 0x4a, 0x16, 0xee, 0x6f, 0xc3, 0xc0, 0xfe,
|
||||
0x56, 0x82, 0x37, 0xa9, 0xb8, 0x3a, 0x66, 0x22, 0xc6, 0xfb, 0xdb, 0x67, 0xc2, 0xfd, 0xdd, 0x62,
|
||||
0x6d, 0x3e, 0xee, 0x3a, 0x1c, 0x2f, 0x2e, 0xaa, 0x69, 0x99, 0x5e, 0xb0, 0x41, 0xc0, 0x8a, 0x81,
|
||||
0x88, 0x7c, 0x9c, 0x84, 0xb5, 0xcf, 0x9f, 0xaf, 0x45, 0x77, 0x9b, 0x6e, 0xe7, 0x55, 0xa5, 0xd7,
|
||||
0x3e, 0xdf, 0xfd, 0x67, 0x78, 0xff, 0x12, 0x38, 0xf1, 0x80, 0xa1, 0x87, 0x9a, 0x93, 0x1b, 0xe0,
|
||||
0x45, 0x3a, 0xcf, 0x2b, 0x53, 0xa8, 0x2f, 0xfa, 0x58, 0x77, 0x14, 0x88, 0xdc, 0xa0, 0x97, 0xa2,
|
||||
0x4d, 0xcb, 0x74, 0xff, 0x34, 0xb2, 0xc3, 0xa4, 0x02, 0x69, 0x59, 0xd3, 0xde, 0x0e, 0x41, 0xa4,
|
||||
0x65, 0x38, 0x09, 0x87, 0xc2, 0x41, 0xc9, 0x17, 0x45, 0xd5, 0x31, 0x14, 0x00, 0x14, 0x1e, 0x0a,
|
||||
0x6d, 0x58, 0xfb, 0x7c, 0x1b, 0xfd, 0xae, 0x3b, 0xfc, 0xdc, 0xc6, 0xde, 0xa6, 0xc7, 0x14, 0xd6,
|
||||
0xc4, 0xc3, 0xbe, 0xb8, 0xcd, 0x28, 0x1a, 0xcf, 0x62, 0x8f, 0x89, 0x38, 0xcd, 0xaa, 0xc1, 0x3a,
|
||||
0x6e, 0xa3, 0x91, 0x13, 0x19, 0x05, 0xc6, 0xc1, 0xf8, 0xb6, 0xb7, 0x28, 0xb2, 0x74, 0xda, 0x7e,
|
||||
0xbc, 0xa3, 0x75, 0x8d, 0x38, 0x1c, 0xdf, 0x5c, 0x0c, 0xc6, 0xeb, 0x3a, 0xf5, 0x93, 0xff, 0x99,
|
||||
0xac, 0x0a, 0x86, 0xc7, 0x6b, 0x0f, 0x09, 0xc7, 0x6b, 0x88, 0xc2, 0xfa, 0x8c, 0x99, 0x38, 0x8a,
|
||||
0x57, 0x7c, 0x41, 0xc4, 0x6b, 0x23, 0x0e, 0xd7, 0xc7, 0xc5, 0xec, 0xde, 0xc0, 0x78, 0x38, 0xcc,
|
||||
0x05, 0x2b, 0xf3, 0x38, 0xdb, 0xcf, 0xe2, 0x59, 0x35, 0x20, 0x62, 0x8c, 0x4f, 0x11, 0x7b, 0x03,
|
||||
0x9a, 0x46, 0x9a, 0xf1, 0xb0, 0xda, 0x8f, 0x97, 0xbc, 0x4c, 0x05, 0xdd, 0x8c, 0x16, 0xe9, 0x6c,
|
||||
0x46, 0x0f, 0x45, 0xbd, 0x8d, 0xca, 0xe9, 0x55, 0xba, 0x64, 0x49, 0xc0, 0x5b, 0x83, 0xf4, 0xf0,
|
||||
0xe6, 0xa0, 0x48, 0xa7, 0x8d, 0xf9, 0xa2, 0x9c, 0x32, 0xb2, 0xd3, 0x94, 0xb8, 0xb3, 0xd3, 0x0c,
|
||||
0xa6, 0x3d, 0xfc, 0xf5, 0x5a, 0xf4, 0x7b, 0x4a, 0xea, 0x3e, 0x73, 0xd9, 0x8b, 0xab, 0xab, 0x0b,
|
||||
0x1e, 0x97, 0xc9, 0xe0, 0x13, 0xcc, 0x0e, 0x8a, 0x1a, 0xd7, 0xcf, 0x6e, 0xa3, 0x02, 0x9b, 0xb5,
|
||||
0xce, 0xbb, 0xed, 0x8c, 0x43, 0x9b, 0xd5, 0x43, 0xc2, 0xcd, 0x0a, 0x51, 0x18, 0x40, 0xa4, 0x5c,
|
||||
0x1d, 0xc9, 0xad, 0x93, 0xfa, 0xfe, 0xb9, 0xdc, 0x46, 0x27, 0x07, 0xe3, 0x63, 0x2d, 0xf4, 0x47,
|
||||
0xcb, 0x36, 0x65, 0x03, 0x1f, 0x31, 0xc3, 0xbe, 0x38, 0xe9, 0xd9, 0xcc, 0x8a, 0xb0, 0xe7, 0xd6,
|
||||
0xcc, 0x18, 0xf6, 0xc5, 0x09, 0xcf, 0x4e, 0x58, 0x0b, 0x79, 0x46, 0x42, 0xdb, 0xb0, 0x2f, 0x0e,
|
||||
0xb3, 0x2f, 0xcd, 0x34, 0xeb, 0xc2, 0x93, 0x80, 0x1d, 0xb8, 0x36, 0x6c, 0xf6, 0x62, 0xb5, 0xc3,
|
||||
0xbf, 0x5d, 0x8b, 0xbe, 0x67, 0x3d, 0x1e, 0xf3, 0x24, 0xbd, 0x5c, 0x29, 0xe8, 0x75, 0x9c, 0x2d,
|
||||
0x58, 0x35, 0x78, 0x46, 0x59, 0x6b, 0xb3, 0xa6, 0x04, 0xcf, 0x6f, 0xa5, 0x03, 0xe7, 0xce, 0xa8,
|
||||
0x28, 0xb2, 0xd5, 0x84, 0xcd, 0x8b, 0x8c, 0x9c, 0x3b, 0x1e, 0x12, 0x9e, 0x3b, 0x10, 0x85, 0x59,
|
||||
0xf9, 0x84, 0xd7, 0x39, 0x3f, 0x9a, 0x95, 0x4b, 0x51, 0x38, 0x2b, 0x6f, 0x10, 0x98, 0x2b, 0x4d,
|
||||
0xf8, 0x2e, 0xcf, 0x32, 0x36, 0x15, 0xed, 0x7b, 0x1b, 0x46, 0xd3, 0x12, 0xe1, 0x5c, 0x09, 0x90,
|
||||
0xf6, 0x54, 0xae, 0xd9, 0x43, 0xc6, 0x25, 0x7b, 0xb1, 0x3a, 0x4a, 0xf3, 0xeb, 0x01, 0x9e, 0x16,
|
||||
0x58, 0x80, 0x38, 0x95, 0x43, 0x41, 0xb8, 0x57, 0x3d, 0xcf, 0x13, 0x8e, 0xef, 0x55, 0x6b, 0x49,
|
||||
0x78, 0xaf, 0xaa, 0x09, 0x68, 0xf2, 0x8c, 0x51, 0x26, 0x6b, 0x49, 0xd8, 0xa4, 0x26, 0xb0, 0x50,
|
||||
0xa8, 0x9f, 0xdd, 0x90, 0xa1, 0x10, 0x3c, 0xad, 0xd9, 0xe8, 0xe4, 0xe0, 0x08, 0x6d, 0x36, 0xad,
|
||||
0xfb, 0x4c, 0x4c, 0xaf, 0xf0, 0x11, 0xea, 0x21, 0xe1, 0x11, 0x0a, 0x51, 0x58, 0xa5, 0x09, 0x37,
|
||||
0x9b, 0xee, 0x75, 0x7c, 0x7c, 0xb4, 0x36, 0xdc, 0x1b, 0x9d, 0x1c, 0xdc, 0x46, 0x1e, 0xce, 0x65,
|
||||
0x9b, 0xa1, 0x83, 0x5c, 0xc9, 0xc2, 0xdb, 0x48, 0xc3, 0xc0, 0xd2, 0x2b, 0x81, 0x3c, 0xcb, 0x5a,
|
||||
0xa7, 0x15, 0xbd, 0xd3, 0xac, 0x8d, 0x4e, 0x4e, 0x3b, 0xf9, 0x57, 0xb3, 0x8d, 0x53, 0xd2, 0x13,
|
||||
0x5e, 0xcf, 0x91, 0xd7, 0x71, 0x96, 0x26, 0xb1, 0x60, 0x13, 0x7e, 0xcd, 0x72, 0x7c, 0xc7, 0xa4,
|
||||
0x4b, 0xab, 0xf8, 0xa1, 0xa7, 0x10, 0xde, 0x31, 0x85, 0x15, 0xe1, 0x38, 0x51, 0xf4, 0x79, 0xc5,
|
||||
0x76, 0xe3, 0x8a, 0x88, 0x64, 0x1e, 0x12, 0x1e, 0x27, 0x10, 0x85, 0xf9, 0xaa, 0x92, 0xbf, 0x7c,
|
||||
0x5b, 0xb0, 0x32, 0x65, 0xf9, 0x94, 0xe1, 0xf9, 0x2a, 0xa4, 0xc2, 0xf9, 0x2a, 0x42, 0xc3, 0xbd,
|
||||
0xda, 0x5e, 0x2c, 0xd8, 0x8b, 0xd5, 0x24, 0x9d, 0xb3, 0x4a, 0xc4, 0xf3, 0x02, 0xdf, 0xab, 0x01,
|
||||
0x28, 0xbc, 0x57, 0x6b, 0xc3, 0xad, 0xa3, 0x21, 0x13, 0x10, 0xdb, 0xd7, 0xbd, 0x20, 0x11, 0xb8,
|
||||
0xee, 0x45, 0xa0, 0xb0, 0x61, 0x2d, 0x80, 0x3e, 0x24, 0x68, 0x59, 0x09, 0x3e, 0x24, 0xa0, 0xe9,
|
||||
0xd6, 0x81, 0x9b, 0x61, 0xc6, 0xf5, 0xd4, 0xec, 0x28, 0xfa, 0xd8, 0x9d, 0xa2, 0x9b, 0xbd, 0x58,
|
||||
0xfc, 0x84, 0xef, 0x8c, 0x65, 0xb1, 0x5c, 0xb6, 0x02, 0xc7, 0x68, 0x0d, 0xd3, 0xe7, 0x84, 0xcf,
|
||||
0x61, 0xb5, 0xc3, 0xbf, 0x5c, 0x8b, 0x3e, 0xc4, 0x3c, 0xbe, 0x2a, 0xa4, 0xdf, 0xa7, 0xdd, 0xb6,
|
||||
0x14, 0x49, 0xdc, 0x67, 0x0b, 0x6b, 0xd8, 0x2b, 0x19, 0x8d, 0xc8, 0x5e, 0x77, 0xd3, 0x05, 0xf0,
|
||||
0x93, 0x36, 0x53, 0x7e, 0xc8, 0x11, 0x57, 0x32, 0x42, 0xbc, 0xdd, 0x0f, 0xf9, 0xe5, 0xaa, 0xc0,
|
||||
0x7e, 0xc8, 0xd8, 0xd0, 0x62, 0x62, 0x3f, 0x84, 0x60, 0x76, 0x76, 0xba, 0xd5, 0x7b, 0x93, 0x8a,
|
||||
0x2b, 0x99, 0x6f, 0x81, 0xd9, 0xe9, 0x95, 0xd5, 0x40, 0xc4, 0xec, 0x24, 0x61, 0x98, 0x91, 0x34,
|
||||
0x60, 0x3d, 0x37, 0xb1, 0x58, 0x6e, 0x0c, 0xb9, 0x33, 0xf3, 0x51, 0x37, 0x08, 0xc7, 0x6b, 0x23,
|
||||
0xd6, 0x5b, 0x9f, 0x27, 0x21, 0x0b, 0x60, 0xfb, 0xb3, 0xd9, 0x8b, 0xd5, 0x0e, 0xff, 0x3c, 0xfa,
|
||||
0x6e, 0xab, 0x62, 0xfb, 0x2c, 0x16, 0x8b, 0x92, 0x25, 0x83, 0x9d, 0x8e, 0x72, 0x37, 0xa0, 0x71,
|
||||
0xfd, 0xb4, 0xbf, 0x42, 0x2b, 0x47, 0x6f, 0x38, 0x35, 0xac, 0x4c, 0x19, 0x9e, 0x85, 0x4c, 0xfa,
|
||||
0x6c, 0x30, 0x47, 0xa7, 0x75, 0x5a, 0xdb, 0x6c, 0x77, 0x74, 0x8d, 0x96, 0x71, 0x9a, 0xc9, 0x87,
|
||||
0xb5, 0x9f, 0x84, 0x8c, 0x7a, 0x68, 0x70, 0x9b, 0x4d, 0xaa, 0xb4, 0x22, 0xb3, 0x9c, 0xe3, 0xce,
|
||||
0xf6, 0x6c, 0x8b, 0x8e, 0x04, 0xc8, 0xee, 0x6c, 0xbb, 0x27, 0xad, 0xdd, 0x8a, 0x66, 0xc9, 0xab,
|
||||
0xff, 0xec, 0x0e, 0x72, 0xcc, 0xab, 0x56, 0x45, 0x46, 0xfa, 0x76, 0x4f, 0x5a, 0x7b, 0xfd, 0xb3,
|
||||
0xe8, 0x83, 0xb6, 0x57, 0xbd, 0x10, 0xed, 0x74, 0x9a, 0x02, 0x6b, 0xd1, 0xd3, 0xfe, 0x0a, 0x76,
|
||||
0x4b, 0xf3, 0x65, 0x5a, 0x09, 0x5e, 0xae, 0xc6, 0x57, 0xfc, 0xa6, 0x79, 0x8d, 0xc4, 0x9f, 0xad,
|
||||
0x1a, 0x18, 0x3a, 0x04, 0xb1, 0xa5, 0xc1, 0xc9, 0x96, 0x2b, 0xfb, 0xba, 0x49, 0x45, 0xb8, 0x72,
|
||||
0x88, 0x0e, 0x57, 0x3e, 0x69, 0x63, 0x55, 0x53, 0x2b, 0xfb, 0x6e, 0xcc, 0x06, 0x5e, 0xd4, 0xf6,
|
||||
0xfb, 0x31, 0x8f, 0xba, 0x41, 0x9b, 0xb1, 0x68, 0xf1, 0x5e, 0x7a, 0x79, 0x69, 0xea, 0x84, 0x97,
|
||||
0xd4, 0x45, 0x88, 0x8c, 0x85, 0x40, 0x6d, 0xd2, 0xbd, 0x9f, 0x66, 0x4c, 0x9e, 0xe8, 0xbf, 0xba,
|
||||
0xbc, 0xcc, 0x78, 0x9c, 0x80, 0xa4, 0xbb, 0x16, 0x0f, 0x5d, 0x39, 0x91, 0x74, 0x63, 0x9c, 0xbd,
|
||||
0x2b, 0x50, 0x4b, 0xcf, 0xd8, 0x94, 0xe7, 0xd3, 0x34, 0x83, 0xb7, 0x50, 0xa5, 0xa6, 0x11, 0x12,
|
||||
0x77, 0x05, 0x5a, 0x90, 0x5d, 0x18, 0x6b, 0x51, 0x3d, 0xed, 0x9b, 0xf2, 0x3f, 0x6c, 0x2b, 0x3a,
|
||||
0x62, 0x62, 0x61, 0x44, 0x30, 0xbb, 0xf7, 0xac, 0x85, 0xe7, 0x85, 0x34, 0x7e, 0xaf, 0xad, 0xa5,
|
||||
0x24, 0xc4, 0xde, 0xd3, 0x27, 0xec, 0x1e, 0xaa, 0xfe, 0xfb, 0x1e, 0xbf, 0xc9, 0xa5, 0xd1, 0xfb,
|
||||
0x6d, 0x95, 0x46, 0x46, 0xec, 0xa1, 0x20, 0xa3, 0x0d, 0xff, 0x24, 0xfa, 0x55, 0x69, 0xb8, 0xe4,
|
||||
0xc5, 0xe0, 0x0e, 0xa2, 0x50, 0x3a, 0x77, 0x36, 0xef, 0x92, 0x72, 0x7b, 0xb5, 0xc0, 0x8c, 0x8d,
|
||||
0xf3, 0x2a, 0x9e, 0xc1, 0x8b, 0xd6, 0xb6, 0xc7, 0xa5, 0x94, 0xb8, 0x5a, 0xd0, 0xa6, 0xfc, 0x51,
|
||||
0x71, 0xc2, 0x13, 0x6d, 0x1d, 0xa9, 0xa1, 0x11, 0x86, 0x46, 0x85, 0x0b, 0xd9, 0x64, 0xe6, 0x24,
|
||||
0x5e, 0xa6, 0x33, 0xb3, 0xe0, 0xa8, 0xb8, 0x55, 0x81, 0x64, 0xc6, 0x32, 0x43, 0x07, 0x22, 0x92,
|
||||
0x19, 0x12, 0xd6, 0x3e, 0xff, 0x65, 0x2d, 0xba, 0x67, 0x99, 0x83, 0xe6, 0xb4, 0xee, 0x30, 0xbf,
|
||||
0xe4, 0x75, 0xea, 0x73, 0x94, 0xe6, 0xd7, 0xd5, 0xe0, 0x73, 0xca, 0x24, 0xce, 0x9b, 0xa2, 0x7c,
|
||||
0x71, 0x6b, 0x3d, 0x9b, 0xb5, 0x36, 0x47, 0x59, 0xf6, 0x79, 0xb6, 0xd2, 0x00, 0x59, 0xab, 0x39,
|
||||
0xf1, 0x82, 0x1c, 0x91, 0xb5, 0x86, 0x78, 0xdb, 0xc5, 0xc6, 0x79, 0xc6, 0x73, 0xd8, 0xc5, 0xd6,
|
||||
0x42, 0x2d, 0x24, 0xba, 0xb8, 0x05, 0xd9, 0x78, 0xdc, 0x88, 0xd4, 0xa9, 0xcb, 0x28, 0xcb, 0x40,
|
||||
0x3c, 0x36, 0xaa, 0x06, 0x20, 0xe2, 0x31, 0x0a, 0x6a, 0x3f, 0x67, 0xd1, 0xb7, 0xeb, 0x26, 0x3d,
|
||||
0x2d, 0xd9, 0x32, 0x65, 0xf0, 0xea, 0x85, 0x23, 0x21, 0xe6, 0xbf, 0x4f, 0xd8, 0x99, 0x75, 0x9e,
|
||||
0x57, 0x45, 0x16, 0x57, 0x57, 0xfa, 0x61, 0xbc, 0x5f, 0xe7, 0x46, 0x08, 0x1f, 0xc7, 0x3f, 0xec,
|
||||
0xa0, 0x6c, 0x50, 0x6f, 0x64, 0x26, 0xc4, 0xac, 0xe3, 0xaa, 0xad, 0x30, 0xb3, 0xd1, 0xc9, 0xd9,
|
||||
0x13, 0xef, 0x83, 0x38, 0xcb, 0x58, 0xb9, 0x6a, 0x64, 0xc7, 0x71, 0x9e, 0x5e, 0xb2, 0x4a, 0x80,
|
||||
0x13, 0x6f, 0x4d, 0x0d, 0x21, 0x46, 0x9c, 0x78, 0x07, 0x70, 0x9b, 0xcd, 0x03, 0xcf, 0x87, 0x79,
|
||||
0xc2, 0xde, 0x82, 0x6c, 0x1e, 0xda, 0x91, 0x0c, 0x91, 0xcd, 0x53, 0xac, 0x3d, 0xf9, 0x7d, 0x91,
|
||||
0xf1, 0xe9, 0xb5, 0x5e, 0x02, 0xfc, 0x0e, 0x96, 0x12, 0xb8, 0x06, 0xdc, 0x0f, 0x21, 0x76, 0x11,
|
||||
0x90, 0x82, 0x33, 0x56, 0x64, 0xf1, 0x14, 0xde, 0xbf, 0x51, 0x3a, 0x5a, 0x46, 0x2c, 0x02, 0x90,
|
||||
0x01, 0xc5, 0xd5, 0xf7, 0x7a, 0xb0, 0xe2, 0x82, 0x6b, 0x3d, 0xf7, 0x43, 0x88, 0x5d, 0x06, 0xa5,
|
||||
0x60, 0x5c, 0x64, 0xa9, 0x00, 0xd3, 0x40, 0x69, 0x48, 0x09, 0x31, 0x0d, 0x7c, 0x02, 0x98, 0x3c,
|
||||
0x66, 0xe5, 0x8c, 0xa1, 0x26, 0xa5, 0x24, 0x68, 0xb2, 0x21, 0xec, 0x65, 0x63, 0x55, 0x77, 0x5e,
|
||||
0xac, 0xc0, 0x65, 0x63, 0x5d, 0x2d, 0x5e, 0xac, 0x88, 0xcb, 0xc6, 0x1e, 0x00, 0x8a, 0x78, 0x1a,
|
||||
0x57, 0x02, 0x2f, 0xa2, 0x94, 0x04, 0x8b, 0xd8, 0x10, 0x76, 0x8d, 0x56, 0x45, 0x5c, 0x08, 0xb0,
|
||||
0x46, 0xeb, 0x02, 0x38, 0x4f, 0xa0, 0xef, 0x92, 0x72, 0x1b, 0x49, 0x54, 0xaf, 0x30, 0xb1, 0x9f,
|
||||
0xb2, 0x2c, 0xa9, 0x40, 0x24, 0xd1, 0xed, 0xde, 0x48, 0x89, 0x48, 0xd2, 0xa6, 0xc0, 0x50, 0xd2,
|
||||
0xe7, 0xe3, 0x58, 0xed, 0xc0, 0xd1, 0xf8, 0xfd, 0x10, 0x62, 0xe3, 0x53, 0x53, 0xe8, 0xdd, 0xb8,
|
||||
0x2c, 0xd3, 0x7a, 0xf1, 0x5f, 0xc7, 0x0b, 0xd4, 0xc8, 0x89, 0xf8, 0x84, 0x71, 0x60, 0x7a, 0x35,
|
||||
0x81, 0x1b, 0x2b, 0x18, 0x0c, 0xdd, 0x1f, 0x07, 0x19, 0x9b, 0x71, 0x4a, 0x89, 0xf3, 0x08, 0x15,
|
||||
0x6b, 0x4d, 0xe4, 0x09, 0xea, 0x7a, 0x17, 0xe6, 0xbc, 0x89, 0x64, 0x5c, 0x1c, 0xf3, 0x25, 0x9b,
|
||||
0xf0, 0x97, 0x6f, 0xd3, 0x4a, 0xa4, 0xf9, 0x4c, 0xaf, 0xdc, 0xcf, 0x09, 0x4b, 0x18, 0x4c, 0xbc,
|
||||
0x89, 0xd4, 0xa9, 0x64, 0x13, 0x08, 0x50, 0x96, 0x13, 0x76, 0x83, 0x26, 0x10, 0xd0, 0xa2, 0xe1,
|
||||
0x88, 0x04, 0x22, 0xc4, 0xdb, 0x73, 0x14, 0xe3, 0x5c, 0xbf, 0x7e, 0x3e, 0xe1, 0x4d, 0x2e, 0x47,
|
||||
0x59, 0x83, 0x20, 0xb1, 0x95, 0x0d, 0x2a, 0xd8, 0xfd, 0xa5, 0xf1, 0x6f, 0xa7, 0xd8, 0x23, 0xc2,
|
||||
0x4e, 0x7b, 0x9a, 0x3d, 0xee, 0x41, 0x22, 0xae, 0xec, 0x3d, 0x00, 0xca, 0x55, 0xfb, 0x1a, 0xc0,
|
||||
0xe3, 0x1e, 0xa4, 0x73, 0x26, 0xe3, 0x56, 0xeb, 0x45, 0x3c, 0xbd, 0x9e, 0x95, 0x7c, 0x91, 0x27,
|
||||
0xbb, 0x3c, 0xe3, 0x25, 0x38, 0x93, 0xf1, 0x4a, 0x0d, 0x50, 0xe2, 0x4c, 0xa6, 0x43, 0xc5, 0x66,
|
||||
0x70, 0x6e, 0x29, 0x46, 0x59, 0x3a, 0x83, 0x3b, 0x6a, 0xcf, 0x90, 0x04, 0x88, 0x0c, 0x0e, 0x05,
|
||||
0x91, 0x41, 0xa4, 0x76, 0xdc, 0x22, 0x9d, 0xc6, 0x99, 0xf2, 0xb7, 0x43, 0x9b, 0xf1, 0xc0, 0xce,
|
||||
0x41, 0x84, 0x28, 0x20, 0xf5, 0x9c, 0x2c, 0xca, 0xfc, 0x30, 0x17, 0x9c, 0xac, 0x67, 0x03, 0x74,
|
||||
0xd6, 0xd3, 0x01, 0x41, 0x58, 0x9d, 0xb0, 0xb7, 0x75, 0x69, 0xea, 0x7f, 0xb0, 0xb0, 0x5a, 0xff,
|
||||
0x7d, 0xa8, 0xe5, 0xa1, 0xb0, 0x0a, 0x38, 0x50, 0x19, 0xed, 0x44, 0x0d, 0x98, 0x80, 0xb6, 0x3f,
|
||||
0x4c, 0x1e, 0x75, 0x83, 0xb8, 0x9f, 0xb1, 0x58, 0x65, 0x2c, 0xe4, 0x47, 0x02, 0x7d, 0xfc, 0x34,
|
||||
0xa0, 0x3d, 0x6e, 0xf1, 0xea, 0x73, 0xc5, 0xa6, 0xd7, 0xad, 0x6b, 0x4d, 0x7e, 0x41, 0x15, 0x42,
|
||||
0x1c, 0xb7, 0x10, 0x28, 0xde, 0x45, 0x87, 0x53, 0x9e, 0x87, 0xba, 0xa8, 0x96, 0xf7, 0xe9, 0x22,
|
||||
0xcd, 0xd9, 0xcd, 0xaf, 0x91, 0xea, 0x91, 0xa9, 0xba, 0x69, 0x93, 0xb0, 0xe0, 0x42, 0xc4, 0xe6,
|
||||
0x97, 0x84, 0x6d, 0x4e, 0x0e, 0x7d, 0x1e, 0xb7, 0xef, 0x7c, 0xb7, 0xac, 0x1c, 0xd3, 0x77, 0xbe,
|
||||
0x29, 0x96, 0xae, 0xa4, 0x1a, 0x23, 0x1d, 0x56, 0xfc, 0x71, 0xb2, 0xd5, 0x0f, 0xb6, 0x5b, 0x1e,
|
||||
0xcf, 0xe7, 0x6e, 0xc6, 0xe2, 0x52, 0x79, 0xdd, 0x0e, 0x18, 0xb2, 0x18, 0xb1, 0xe5, 0x09, 0xe0,
|
||||
0x20, 0x84, 0x79, 0x9e, 0x77, 0x79, 0x2e, 0x58, 0x2e, 0xb0, 0x10, 0xe6, 0x1b, 0xd3, 0x60, 0x28,
|
||||
0x84, 0x51, 0x0a, 0x60, 0xdc, 0xca, 0xf3, 0x20, 0x26, 0x4e, 0xe2, 0x39, 0x9a, 0xb1, 0xa9, 0xb3,
|
||||
0x1e, 0x25, 0x0f, 0x8d, 0x5b, 0xc0, 0x39, 0x0f, 0xf9, 0x5c, 0x2f, 0x93, 0xb8, 0x9c, 0x99, 0xd3,
|
||||
0x8d, 0x64, 0xf0, 0x94, 0xb6, 0xe3, 0x93, 0xc4, 0x43, 0xbe, 0xb0, 0x06, 0x08, 0x3b, 0x87, 0xf3,
|
||||
0x78, 0x66, 0x6a, 0x8a, 0xd4, 0x40, 0xca, 0x5b, 0x55, 0x7d, 0xd4, 0x0d, 0x02, 0x3f, 0xaf, 0xd3,
|
||||
0x84, 0xf1, 0x80, 0x1f, 0x29, 0xef, 0xe3, 0x07, 0x82, 0x20, 0x7b, 0xab, 0xeb, 0xad, 0x76, 0x74,
|
||||
0xa3, 0x3c, 0xd1, 0xfb, 0xd8, 0x21, 0xd1, 0x3c, 0x80, 0x0b, 0x65, 0x6f, 0x04, 0x0f, 0xe6, 0x68,
|
||||
0x73, 0x40, 0x1b, 0x9a, 0xa3, 0xe6, 0xfc, 0xb5, 0xcf, 0x1c, 0xc5, 0x60, 0xed, 0xf3, 0x67, 0x7a,
|
||||
0x8e, 0xee, 0xc5, 0x22, 0xae, 0xf3, 0xf6, 0xd7, 0x29, 0xbb, 0xd1, 0x1b, 0x61, 0xa4, 0xbe, 0x0d,
|
||||
0x35, 0x94, 0xaf, 0xac, 0x82, 0x5d, 0xf1, 0x4e, 0x6f, 0x3e, 0xe0, 0x5b, 0xef, 0x10, 0x3a, 0x7d,
|
||||
0x83, 0xad, 0xc2, 0x4e, 0x6f, 0x3e, 0xe0, 0x5b, 0xbf, 0x0b, 0xdf, 0xe9, 0x1b, 0xbc, 0x10, 0xbf,
|
||||
0xd3, 0x9b, 0xd7, 0xbe, 0xff, 0xaa, 0x99, 0xb8, 0xae, 0xf3, 0x3a, 0x0f, 0x9b, 0x8a, 0x74, 0xc9,
|
||||
0xb0, 0x74, 0xd2, 0xb7, 0x67, 0xd0, 0x50, 0x3a, 0x49, 0xab, 0x38, 0x5f, 0xa3, 0xc2, 0x4a, 0x71,
|
||||
0xca, 0xab, 0x54, 0x3e, 0xa4, 0x7f, 0xde, 0xc3, 0x68, 0x03, 0x87, 0x36, 0x4d, 0x21, 0x25, 0xfb,
|
||||
0xb8, 0xd1, 0x43, 0xed, 0x2d, 0xe6, 0xad, 0x80, 0xbd, 0xf6, 0x65, 0xe6, 0xed, 0x9e, 0xb4, 0x7d,
|
||||
0xf0, 0xe7, 0x31, 0xee, 0x13, 0xc7, 0x50, 0xaf, 0xa2, 0x0f, 0x1d, 0x9f, 0xf6, 0x57, 0xd0, 0xee,
|
||||
0xff, 0xa6, 0xd9, 0x57, 0x40, 0xff, 0x7a, 0x12, 0x3c, 0xeb, 0x63, 0x11, 0x4c, 0x84, 0xe7, 0xb7,
|
||||
0xd2, 0xd1, 0x05, 0xf9, 0x87, 0x66, 0x03, 0xdd, 0xa0, 0xf2, 0x5d, 0x0e, 0xf9, 0x0e, 0xa8, 0x9e,
|
||||
0x13, 0xa1, 0x6e, 0xb5, 0x30, 0x9c, 0x19, 0x9f, 0xdd, 0x52, 0xcb, 0xf9, 0x36, 0x99, 0x07, 0xeb,
|
||||
0x77, 0x0e, 0x9d, 0xf2, 0x84, 0x2c, 0x3b, 0x34, 0x2c, 0xd0, 0xe7, 0xb7, 0x55, 0xa3, 0xe6, 0x8a,
|
||||
0x03, 0xcb, 0xaf, 0x73, 0x3c, 0xef, 0x69, 0xd8, 0xfb, 0x5e, 0xc7, 0xa7, 0xb7, 0x53, 0xd2, 0x65,
|
||||
0xf9, 0xcf, 0xb5, 0xe8, 0xa1, 0xc7, 0xda, 0xe7, 0x09, 0xe0, 0xd4, 0xe3, 0x47, 0x01, 0xfb, 0x94,
|
||||
0x92, 0x29, 0xdc, 0xef, 0xff, 0x72, 0xca, 0xf6, 0x43, 0x5e, 0x9e, 0xca, 0x7e, 0x9a, 0x09, 0x56,
|
||||
0xb6, 0x3f, 0xe4, 0xe5, 0xdb, 0x55, 0xd4, 0x90, 0xfe, 0x90, 0x57, 0x00, 0x77, 0x3e, 0xe4, 0x85,
|
||||
0x78, 0x46, 0x3f, 0xe4, 0x85, 0x5a, 0x0b, 0x7e, 0xc8, 0x2b, 0xac, 0x41, 0x85, 0xf7, 0xa6, 0x08,
|
||||
0xea, 0xdc, 0xba, 0x97, 0x45, 0xff, 0x18, 0xfb, 0xd9, 0x6d, 0x54, 0x88, 0x05, 0x4e, 0x71, 0xf2,
|
||||
0x9e, 0x5b, 0x8f, 0x36, 0xf5, 0xee, 0xba, 0xed, 0xf4, 0xe6, 0xb5, 0xef, 0x9f, 0xea, 0xdd, 0x8d,
|
||||
0x09, 0xe7, 0xbc, 0x94, 0x1f, 0x71, 0xdb, 0x0c, 0x85, 0xe7, 0xda, 0x82, 0xdb, 0xf3, 0x5b, 0xfd,
|
||||
0x60, 0xa2, 0xba, 0x35, 0xa1, 0x3b, 0x7d, 0xd8, 0x65, 0x08, 0x74, 0xf9, 0x4e, 0x6f, 0x9e, 0x58,
|
||||
0x46, 0x94, 0x6f, 0xd5, 0xdb, 0x3d, 0x8c, 0xf9, 0x7d, 0xfd, 0xb4, 0xbf, 0x82, 0x76, 0xbf, 0xd4,
|
||||
0x69, 0xa3, 0xeb, 0x5e, 0xf6, 0xf3, 0x76, 0x97, 0xa9, 0xb1, 0xd7, 0xcd, 0xc3, 0xbe, 0x78, 0x28,
|
||||
0x81, 0x70, 0x97, 0xd0, 0xae, 0x04, 0x02, 0x5d, 0x46, 0x3f, 0xbd, 0x9d, 0x92, 0x2e, 0xcb, 0x3f,
|
||||
0xaf, 0x45, 0x77, 0xc9, 0xb2, 0xe8, 0x71, 0xf0, 0x79, 0x5f, 0xcb, 0x60, 0x3c, 0x7c, 0x71, 0x6b,
|
||||
0x3d, 0x5d, 0xa8, 0x7f, 0x5b, 0x8b, 0xee, 0x05, 0x0a, 0xa5, 0x06, 0xc8, 0x2d, 0xac, 0xfb, 0x03,
|
||||
0xe5, 0x07, 0xb7, 0x57, 0xa4, 0x96, 0x7b, 0x17, 0x1f, 0xb7, 0x3f, 0xca, 0x14, 0xb0, 0x3d, 0xa6,
|
||||
0x3f, 0xca, 0xd4, 0xad, 0x05, 0x0f, 0x79, 0xe2, 0x8b, 0x66, 0xd3, 0x85, 0x1e, 0xf2, 0xc8, 0x1b,
|
||||
0x6a, 0xc1, 0x8f, 0x4b, 0x60, 0x1c, 0xe6, 0xe4, 0xe5, 0xdb, 0x22, 0xce, 0x13, 0xda, 0x89, 0x92,
|
||||
0x77, 0x3b, 0x31, 0x1c, 0x3c, 0x1c, 0xab, 0xa5, 0x67, 0xbc, 0xd9, 0x48, 0x3d, 0xa6, 0xf4, 0x0d,
|
||||
0x12, 0x3c, 0x1c, 0x6b, 0xa1, 0x84, 0x37, 0x9d, 0x35, 0x86, 0xbc, 0x81, 0x64, 0xf1, 0x49, 0x1f,
|
||||
0x14, 0xa4, 0xe8, 0xc6, 0x9b, 0x39, 0x73, 0xdf, 0x0a, 0x59, 0x69, 0x9d, 0xbb, 0x6f, 0xf7, 0xa4,
|
||||
0x09, 0xb7, 0x63, 0x26, 0xbe, 0x64, 0x71, 0xc2, 0xca, 0xa0, 0x5b, 0x43, 0xf5, 0x72, 0xeb, 0xd2,
|
||||
0x98, 0xdb, 0x5d, 0x9e, 0x2d, 0xe6, 0xb9, 0xee, 0x4c, 0xd2, 0xad, 0x4b, 0x75, 0xbb, 0x05, 0x34,
|
||||
0x3c, 0x16, 0xb4, 0x6e, 0x65, 0x7a, 0xf9, 0x24, 0x6c, 0xc6, 0xcb, 0x2a, 0x37, 0x7b, 0xb1, 0x74,
|
||||
0x3d, 0xf5, 0x30, 0xea, 0xa8, 0x27, 0x18, 0x49, 0xdb, 0x3d, 0x69, 0x78, 0x3e, 0xe7, 0xb8, 0x35,
|
||||
0xe3, 0x69, 0xa7, 0xc3, 0x56, 0x6b, 0x48, 0x3d, 0xed, 0xaf, 0x00, 0x4f, 0x43, 0xf5, 0xa8, 0x3a,
|
||||
0x4a, 0x2b, 0xb1, 0x9f, 0x66, 0xd9, 0x60, 0x33, 0x30, 0x4c, 0x1a, 0x28, 0x78, 0x1a, 0x8a, 0xc0,
|
||||
0xc4, 0x48, 0x6e, 0x4e, 0x0f, 0xf3, 0x41, 0x97, 0x1d, 0x49, 0xf5, 0x1a, 0xc9, 0x2e, 0x0d, 0x4e,
|
||||
0xb4, 0x9c, 0xa6, 0x36, 0xb5, 0x1d, 0x86, 0x1b, 0xae, 0x55, 0xe1, 0x9d, 0xde, 0x3c, 0x78, 0xdc,
|
||||
0x2e, 0x29, 0xb9, 0xb2, 0x3c, 0xa0, 0x4c, 0x78, 0x2b, 0xc9, 0xc3, 0x0e, 0x0a, 0x9c, 0x0a, 0xaa,
|
||||
0x69, 0xf4, 0x26, 0x4d, 0x66, 0x4c, 0xa0, 0x4f, 0x8a, 0x5c, 0x20, 0xf8, 0xa4, 0x08, 0x80, 0xa0,
|
||||
0xeb, 0xd4, 0xdf, 0xcd, 0x71, 0xe8, 0x61, 0x82, 0x75, 0x9d, 0x56, 0x76, 0xa8, 0x50, 0xd7, 0xa1,
|
||||
0x34, 0x88, 0x06, 0xc6, 0xad, 0x7e, 0x1d, 0xff, 0x49, 0xc8, 0x0c, 0x78, 0x27, 0x7f, 0xb3, 0x17,
|
||||
0x0b, 0x56, 0x14, 0xeb, 0x30, 0x9d, 0xa7, 0x02, 0x5b, 0x51, 0x1c, 0x1b, 0x35, 0x12, 0x5a, 0x51,
|
||||
0xda, 0x28, 0x55, 0xbd, 0x3a, 0x47, 0x38, 0x4c, 0xc2, 0xd5, 0x53, 0x4c, 0xbf, 0xea, 0x19, 0xb6,
|
||||
0xf5, 0x60, 0x33, 0x37, 0x43, 0x46, 0x5c, 0xe9, 0xcd, 0x32, 0x32, 0xb6, 0xe5, 0x6b, 0x9a, 0x10,
|
||||
0x0c, 0x45, 0x1d, 0x4a, 0x01, 0x1e, 0xd8, 0xd7, 0x5c, 0xf3, 0xec, 0xb5, 0x28, 0x58, 0x5c, 0xc6,
|
||||
0xf9, 0x14, 0xdd, 0x9c, 0x4a, 0x83, 0x2d, 0x32, 0xb4, 0x39, 0x25, 0x35, 0xc0, 0x63, 0x73, 0xff,
|
||||
0x05, 0x4b, 0x64, 0x2a, 0x98, 0x37, 0x19, 0xfd, 0xf7, 0x2b, 0x1f, 0xf7, 0x20, 0xe1, 0x63, 0xf3,
|
||||
0x06, 0x30, 0x07, 0xdf, 0xca, 0xe9, 0x27, 0x01, 0x53, 0x3e, 0x1a, 0xda, 0x08, 0xd3, 0x2a, 0x60,
|
||||
0x50, 0x9b, 0x04, 0x97, 0x89, 0x9f, 0xb0, 0x15, 0x36, 0xa8, 0x6d, 0x7e, 0x2a, 0x91, 0xd0, 0xa0,
|
||||
0x6e, 0xa3, 0x20, 0xcf, 0x74, 0xf7, 0x41, 0xeb, 0x01, 0x7d, 0x77, 0xeb, 0xb3, 0xd1, 0xc9, 0x81,
|
||||
0x99, 0xb3, 0x97, 0x2e, 0xbd, 0xe7, 0x04, 0x48, 0x41, 0xf7, 0xd2, 0x25, 0xfe, 0x98, 0x60, 0xb3,
|
||||
0x17, 0x0b, 0x1f, 0xc9, 0xc7, 0x82, 0xbd, 0x6d, 0x9e, 0x95, 0x23, 0xc5, 0x95, 0xf2, 0xd6, 0xc3,
|
||||
0xf2, 0x47, 0xdd, 0xa0, 0xbd, 0x00, 0x7b, 0x5a, 0xf2, 0x29, 0xab, 0x2a, 0xfd, 0xa5, 0x4a, 0xff,
|
||||
0x86, 0x91, 0x96, 0x0d, 0xc1, 0x77, 0x2a, 0x1f, 0x84, 0x21, 0xe7, 0xf3, 0x72, 0x4a, 0x64, 0xbf,
|
||||
0x7a, 0xb3, 0x8e, 0x6a, 0xb6, 0x3f, 0x78, 0xb3, 0xd1, 0xc9, 0xd9, 0xe9, 0xa5, 0xa5, 0xee, 0x67,
|
||||
0x6e, 0x1e, 0xa1, 0xea, 0xd8, 0x17, 0x6e, 0x1e, 0xf7, 0x20, 0xb5, 0xab, 0x2f, 0xa3, 0x77, 0x8e,
|
||||
0xf8, 0x6c, 0xcc, 0xf2, 0x64, 0xf0, 0x7d, 0xff, 0x0a, 0x2d, 0x9f, 0x0d, 0xeb, 0x3f, 0x1b, 0xa3,
|
||||
0x77, 0x28, 0xb1, 0xbd, 0x04, 0xb8, 0xc7, 0x2e, 0x16, 0xb3, 0xb1, 0x88, 0x05, 0xb8, 0x04, 0x28,
|
||||
0xff, 0x3e, 0xac, 0x05, 0xc4, 0x25, 0x40, 0x0f, 0x00, 0xf6, 0x26, 0x25, 0x63, 0xa8, 0xbd, 0x5a,
|
||||
0x10, 0xb4, 0xa7, 0x01, 0x9b, 0x45, 0x18, 0x7b, 0x75, 0xa2, 0x0e, 0x2f, 0xed, 0x59, 0x1d, 0x29,
|
||||
0x25, 0xb2, 0x88, 0x36, 0x65, 0x07, 0xb7, 0xaa, 0xbe, 0xfc, 0xea, 0xc8, 0x62, 0x3e, 0x8f, 0xcb,
|
||||
0x15, 0x18, 0xdc, 0xba, 0x96, 0x0e, 0x40, 0x0c, 0x6e, 0x14, 0xb4, 0xb3, 0xb6, 0x69, 0xe6, 0xe9,
|
||||
0xf5, 0x01, 0x2f, 0xf9, 0x42, 0xa4, 0x39, 0x83, 0x5f, 0x9e, 0x30, 0x0d, 0xea, 0x32, 0xc4, 0xac,
|
||||
0xa5, 0x58, 0x9b, 0xe5, 0x4a, 0x42, 0xdd, 0x27, 0x94, 0x1f, 0xcf, 0xae, 0x04, 0x2f, 0xe1, 0xf3,
|
||||
0x44, 0x65, 0x05, 0x42, 0x44, 0x96, 0x4b, 0xc2, 0xa0, 0xef, 0x4f, 0xd3, 0x7c, 0x86, 0xf6, 0xfd,
|
||||
0xa9, 0xfb, 0xf5, 0xd7, 0x7b, 0x34, 0x60, 0x27, 0x94, 0x6a, 0x34, 0x35, 0x01, 0xf4, 0xbb, 0x9c,
|
||||
0x68, 0xa3, 0xbb, 0x04, 0x31, 0xa1, 0x70, 0x12, 0xb8, 0x7a, 0x55, 0xb0, 0x9c, 0x25, 0xcd, 0xad,
|
||||
0x39, 0xcc, 0x95, 0x47, 0x04, 0x5d, 0x41, 0xd2, 0xc6, 0x22, 0x29, 0x3f, 0x5b, 0xe4, 0xa7, 0x25,
|
||||
0xbf, 0x4c, 0x33, 0x56, 0x82, 0x58, 0xa4, 0xd4, 0x1d, 0x39, 0x11, 0x8b, 0x30, 0xce, 0x5e, 0xbf,
|
||||
0x90, 0x52, 0xef, 0x0b, 0xf0, 0x93, 0x32, 0x9e, 0xc2, 0xeb, 0x17, 0xca, 0x46, 0x1b, 0x23, 0x4e,
|
||||
0x06, 0x03, 0xb8, 0x93, 0xe8, 0x28, 0xd7, 0xf9, 0x4a, 0x8e, 0x0f, 0xfd, 0x2e, 0xa1, 0xfc, 0x26,
|
||||
0x6a, 0x05, 0x12, 0x1d, 0x6d, 0x0e, 0x23, 0x89, 0x44, 0x27, 0xac, 0x61, 0x97, 0x12, 0xc9, 0x9d,
|
||||
0xe8, 0x6b, 0x45, 0x60, 0x29, 0x51, 0x36, 0x1a, 0x21, 0xb1, 0x94, 0xb4, 0x20, 0x10, 0x90, 0x9a,
|
||||
0x69, 0x30, 0x43, 0x03, 0x92, 0x91, 0x06, 0x03, 0x92, 0x4b, 0xd9, 0x40, 0x71, 0x98, 0xa7, 0x22,
|
||||
0x8d, 0xb3, 0x31, 0x13, 0xa7, 0x71, 0x19, 0xcf, 0x99, 0x60, 0x25, 0x0c, 0x14, 0x1a, 0x19, 0x7a,
|
||||
0x0c, 0x11, 0x28, 0x28, 0x56, 0x3b, 0xfc, 0x83, 0xe8, 0xbd, 0x7a, 0xdd, 0x67, 0xb9, 0xfe, 0xed,
|
||||
0x9a, 0x97, 0xf2, 0x47, 0xaf, 0x06, 0xef, 0x1b, 0x1b, 0x63, 0x51, 0xb2, 0x78, 0xde, 0xd8, 0x7e,
|
||||
0xd7, 0xfc, 0x5d, 0x82, 0x4f, 0xd7, 0xea, 0xf1, 0x7c, 0xc2, 0x45, 0x7a, 0x59, 0x6f, 0xb3, 0xf5,
|
||||
0x1b, 0x44, 0x60, 0x3c, 0xbb, 0xe2, 0x61, 0xe0, 0x5b, 0x14, 0x18, 0x67, 0xe3, 0xb4, 0x2b, 0x3d,
|
||||
0x63, 0x45, 0x06, 0xe3, 0xb4, 0xa7, 0x2d, 0x01, 0x22, 0x4e, 0xa3, 0xa0, 0x9d, 0x9c, 0xae, 0x78,
|
||||
0xc2, 0xc2, 0x95, 0x99, 0xb0, 0x7e, 0x95, 0x99, 0x78, 0x2f, 0x65, 0x64, 0xd1, 0x7b, 0xc7, 0x6c,
|
||||
0x7e, 0xc1, 0xca, 0xea, 0x2a, 0x2d, 0xa8, 0xef, 0xb6, 0x5a, 0xa2, 0xf3, 0xbb, 0xad, 0x04, 0x6a,
|
||||
0x57, 0x02, 0x0b, 0x1c, 0x56, 0x27, 0xf1, 0x9c, 0xc9, 0x2f, 0x6b, 0x80, 0x95, 0xc0, 0x31, 0xe2,
|
||||
0x40, 0xc4, 0x4a, 0x40, 0xc2, 0xce, 0xfb, 0x5d, 0x96, 0x39, 0x63, 0xb3, 0x7a, 0x84, 0x95, 0xa7,
|
||||
0xf1, 0x6a, 0xce, 0x72, 0xa1, 0x4d, 0x82, 0x33, 0x79, 0xc7, 0x24, 0xce, 0x13, 0x67, 0xf2, 0x7d,
|
||||
0xf4, 0x9c, 0xd0, 0xe4, 0x35, 0xfc, 0x29, 0x2f, 0x85, 0xfa, 0x65, 0xaa, 0xf3, 0x32, 0x03, 0xa1,
|
||||
0xc9, 0x6f, 0x54, 0x8f, 0x24, 0x42, 0x53, 0x58, 0xc3, 0xf9, 0x15, 0x02, 0xaf, 0x0c, 0xaf, 0x59,
|
||||
0x69, 0xc6, 0xc9, 0xcb, 0x79, 0x9c, 0x66, 0x7a, 0x34, 0xfc, 0x30, 0x60, 0x9b, 0xd0, 0x21, 0x7e,
|
||||
0x85, 0xa0, 0xaf, 0xae, 0xf3, 0xbb, 0x0d, 0xe1, 0x12, 0x82, 0x47, 0x04, 0x1d, 0xf6, 0x89, 0x47,
|
||||
0x04, 0xdd, 0x5a, 0x76, 0xe7, 0x6e, 0x59, 0xc9, 0xad, 0x24, 0xb1, 0xcb, 0x13, 0x78, 0x5e, 0xe8,
|
||||
0xd8, 0x04, 0x20, 0xb1, 0x73, 0x0f, 0x2a, 0xd8, 0xd4, 0xc0, 0x62, 0xfb, 0x69, 0x1e, 0x67, 0xe9,
|
||||
0xcf, 0x60, 0x5a, 0xef, 0xd8, 0x69, 0x08, 0x22, 0x35, 0xc0, 0x49, 0xcc, 0xd5, 0x01, 0x13, 0x93,
|
||||
0xb4, 0x0e, 0xfd, 0x8f, 0x02, 0xed, 0x26, 0x89, 0x6e, 0x57, 0x0e, 0xe9, 0x7c, 0xa3, 0x15, 0x36,
|
||||
0xeb, 0xa8, 0x28, 0xc6, 0xf5, 0xaa, 0x7a, 0xc6, 0xa6, 0x2c, 0x2d, 0xc4, 0xe0, 0xb3, 0x70, 0x5b,
|
||||
0x01, 0x9c, 0xb8, 0x68, 0xd1, 0x43, 0xcd, 0x79, 0x7c, 0x5f, 0xc7, 0x92, 0xb1, 0xfa, 0xc9, 0xc6,
|
||||
0xf3, 0x8a, 0x95, 0x3a, 0xd1, 0x38, 0x60, 0x02, 0xcc, 0x4e, 0x87, 0x1b, 0x3a, 0x60, 0x5d, 0x51,
|
||||
0x62, 0x76, 0x86, 0x35, 0xec, 0x61, 0x9f, 0xc3, 0xe9, 0x6f, 0x6e, 0xcb, 0xfb, 0x86, 0x5b, 0xa4,
|
||||
0x31, 0x87, 0x22, 0x0e, 0xfb, 0x68, 0xda, 0x66, 0x6b, 0x6d, 0xb7, 0xa3, 0x7c, 0x75, 0x08, 0xaf,
|
||||
0x4c, 0x20, 0x96, 0x24, 0x46, 0x64, 0x6b, 0x01, 0xdc, 0x39, 0x0c, 0x2f, 0x79, 0x9c, 0x4c, 0xe3,
|
||||
0x4a, 0x9c, 0xc6, 0xab, 0x8c, 0xc7, 0x89, 0x5c, 0xd7, 0xe1, 0x61, 0x78, 0xc3, 0x0c, 0x5d, 0x88,
|
||||
0x3a, 0x0c, 0xa7, 0x60, 0x37, 0x3b, 0x93, 0xbf, 0x44, 0xa9, 0xef, 0x72, 0xc2, 0xec, 0x4c, 0x96,
|
||||
0x17, 0xde, 0xe3, 0x7c, 0x10, 0x86, 0xec, 0x3b, 0x68, 0x4a, 0x24, 0xd3, 0x90, 0x7b, 0x98, 0x8e,
|
||||
0x97, 0x80, 0x7c, 0x14, 0x20, 0xec, 0x77, 0x29, 0xd4, 0xdf, 0x9b, 0x1f, 0x1f, 0x12, 0xfa, 0x4b,
|
||||
0xd6, 0x5b, 0x98, 0xae, 0x0b, 0x0d, 0xdd, 0x0f, 0xdc, 0x6d, 0xf7, 0xa4, 0x6d, 0x9a, 0xb9, 0x7b,
|
||||
0x15, 0x8b, 0x51, 0x92, 0x1c, 0xb3, 0x0a, 0x79, 0xa1, 0xbc, 0x16, 0x0e, 0xad, 0x94, 0x48, 0x33,
|
||||
0xdb, 0x94, 0x1d, 0xe8, 0xb5, 0xec, 0x65, 0x92, 0x0a, 0x2d, 0x6b, 0x6e, 0x48, 0x6f, 0xb5, 0x0d,
|
||||
0xb4, 0x29, 0xa2, 0x56, 0x34, 0x6d, 0x63, 0x79, 0xcd, 0x4c, 0xf8, 0x6c, 0x96, 0x31, 0x0d, 0x9d,
|
||||
0xb1, 0x58, 0x7d, 0xc8, 0x6f, 0xa7, 0x6d, 0x0b, 0x05, 0x89, 0x58, 0x1e, 0x54, 0xb0, 0x69, 0x64,
|
||||
0x8d, 0xa9, 0x47, 0x52, 0x4d, 0xc3, 0x6e, 0xb4, 0xcd, 0x78, 0x00, 0x91, 0x46, 0xa2, 0xa0, 0x7d,
|
||||
0xef, 0xad, 0x16, 0x1f, 0xb0, 0xa6, 0x25, 0xe0, 0x27, 0x88, 0xa4, 0xb2, 0x23, 0x26, 0xde, 0x7b,
|
||||
0x43, 0x30, 0xbb, 0x4f, 0x00, 0x1e, 0x5e, 0xac, 0x0e, 0x13, 0xb8, 0x4f, 0x80, 0xfa, 0x92, 0x21,
|
||||
0xf6, 0x09, 0x14, 0xeb, 0x77, 0x9d, 0x39, 0xf7, 0x3a, 0x8a, 0x2b, 0x5b, 0x39, 0xa4, 0xeb, 0x50,
|
||||
0x30, 0xd4, 0x75, 0x94, 0x82, 0xdf, 0xa4, 0xee, 0xd1, 0x1a, 0xd2, 0xa4, 0xd8, 0xb9, 0xda, 0x7a,
|
||||
0x17, 0xe6, 0x24, 0x3e, 0x5e, 0x15, 0x27, 0x5c, 0x17, 0x43, 0xbf, 0xd7, 0x58, 0x81, 0xc4, 0xc7,
|
||||
0x2f, 0x76, 0x8b, 0x26, 0x12, 0x9f, 0x6e, 0x2d, 0x1b, 0x27, 0xcd, 0xfe, 0x56, 0x5e, 0xa1, 0xc2,
|
||||
0x7f, 0x51, 0x40, 0x09, 0x89, 0x38, 0xd9, 0x82, 0x94, 0xed, 0x17, 0x1f, 0xfd, 0xd7, 0xd7, 0x77,
|
||||
0xd6, 0x7e, 0xf1, 0xf5, 0x9d, 0xb5, 0xff, 0xfd, 0xfa, 0xce, 0xda, 0xcf, 0xbf, 0xb9, 0xf3, 0xad,
|
||||
0x5f, 0x7c, 0x73, 0xe7, 0x5b, 0xff, 0xf3, 0xcd, 0x9d, 0x6f, 0x7d, 0xf5, 0x8e, 0xfe, 0xc5, 0xe4,
|
||||
0x8b, 0x5f, 0x91, 0xbf, 0x7b, 0xfc, 0xfc, 0xff, 0x03, 0x00, 0x00, 0xff, 0xff, 0x5a, 0x7a, 0x62,
|
||||
0x93, 0x55, 0x79, 0x00, 0x00,
|
||||
}
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
|
@ -399,11 +403,14 @@ type ClientCommandsHandler interface {
|
|||
// Account
|
||||
// ***
|
||||
AccountRecover(context.Context, *pb.RpcAccountRecoverRequest) *pb.RpcAccountRecoverResponse
|
||||
AccountMigrate(context.Context, *pb.RpcAccountMigrateRequest) *pb.RpcAccountMigrateResponse
|
||||
AccountMigrateCancel(context.Context, *pb.RpcAccountMigrateCancelRequest) *pb.RpcAccountMigrateCancelResponse
|
||||
AccountCreate(context.Context, *pb.RpcAccountCreateRequest) *pb.RpcAccountCreateResponse
|
||||
AccountDelete(context.Context, *pb.RpcAccountDeleteRequest) *pb.RpcAccountDeleteResponse
|
||||
AccountRevertDeletion(context.Context, *pb.RpcAccountRevertDeletionRequest) *pb.RpcAccountRevertDeletionResponse
|
||||
AccountSelect(context.Context, *pb.RpcAccountSelectRequest) *pb.RpcAccountSelectResponse
|
||||
AccountEnableLocalNetworkSync(context.Context, *pb.RpcAccountEnableLocalNetworkSyncRequest) *pb.RpcAccountEnableLocalNetworkSyncResponse
|
||||
AccountChangeJsonApiAddr(context.Context, *pb.RpcAccountChangeJsonApiAddrRequest) *pb.RpcAccountChangeJsonApiAddrResponse
|
||||
AccountStop(context.Context, *pb.RpcAccountStopRequest) *pb.RpcAccountStopResponse
|
||||
AccountMove(context.Context, *pb.RpcAccountMoveRequest) *pb.RpcAccountMoveResponse
|
||||
AccountConfigUpdate(context.Context, *pb.RpcAccountConfigUpdateRequest) *pb.RpcAccountConfigUpdateResponse
|
||||
|
@ -1137,6 +1144,46 @@ func AccountRecover(b []byte) (resp []byte) {
|
|||
return resp
|
||||
}
|
||||
|
||||
func AccountMigrate(b []byte) (resp []byte) {
|
||||
defer func() {
|
||||
if PanicHandler != nil {
|
||||
if r := recover(); r != nil {
|
||||
resp, _ = (&pb.RpcAccountMigrateResponse{Error: &pb.RpcAccountMigrateResponseError{Code: pb.RpcAccountMigrateResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
|
||||
PanicHandler(r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
in := new(pb.RpcAccountMigrateRequest)
|
||||
if err := in.Unmarshal(b); err != nil {
|
||||
resp, _ = (&pb.RpcAccountMigrateResponse{Error: &pb.RpcAccountMigrateResponseError{Code: pb.RpcAccountMigrateResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
|
||||
return resp
|
||||
}
|
||||
|
||||
resp, _ = clientCommandsHandler.AccountMigrate(context.Background(), in).Marshal()
|
||||
return resp
|
||||
}
|
||||
|
||||
func AccountMigrateCancel(b []byte) (resp []byte) {
|
||||
defer func() {
|
||||
if PanicHandler != nil {
|
||||
if r := recover(); r != nil {
|
||||
resp, _ = (&pb.RpcAccountMigrateCancelResponse{Error: &pb.RpcAccountMigrateCancelResponseError{Code: pb.RpcAccountMigrateCancelResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
|
||||
PanicHandler(r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
in := new(pb.RpcAccountMigrateCancelRequest)
|
||||
if err := in.Unmarshal(b); err != nil {
|
||||
resp, _ = (&pb.RpcAccountMigrateCancelResponse{Error: &pb.RpcAccountMigrateCancelResponseError{Code: pb.RpcAccountMigrateCancelResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
|
||||
return resp
|
||||
}
|
||||
|
||||
resp, _ = clientCommandsHandler.AccountMigrateCancel(context.Background(), in).Marshal()
|
||||
return resp
|
||||
}
|
||||
|
||||
func AccountCreate(b []byte) (resp []byte) {
|
||||
defer func() {
|
||||
if PanicHandler != nil {
|
||||
|
@ -1237,6 +1284,26 @@ func AccountEnableLocalNetworkSync(b []byte) (resp []byte) {
|
|||
return resp
|
||||
}
|
||||
|
||||
func AccountChangeJsonApiAddr(b []byte) (resp []byte) {
|
||||
defer func() {
|
||||
if PanicHandler != nil {
|
||||
if r := recover(); r != nil {
|
||||
resp, _ = (&pb.RpcAccountChangeJsonApiAddrResponse{Error: &pb.RpcAccountChangeJsonApiAddrResponseError{Code: pb.RpcAccountChangeJsonApiAddrResponseError_UNKNOWN_ERROR, Description: "panic recovered"}}).Marshal()
|
||||
PanicHandler(r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
in := new(pb.RpcAccountChangeJsonApiAddrRequest)
|
||||
if err := in.Unmarshal(b); err != nil {
|
||||
resp, _ = (&pb.RpcAccountChangeJsonApiAddrResponse{Error: &pb.RpcAccountChangeJsonApiAddrResponseError{Code: pb.RpcAccountChangeJsonApiAddrResponseError_BAD_INPUT, Description: err.Error()}}).Marshal()
|
||||
return resp
|
||||
}
|
||||
|
||||
resp, _ = clientCommandsHandler.AccountChangeJsonApiAddr(context.Background(), in).Marshal()
|
||||
return resp
|
||||
}
|
||||
|
||||
func AccountStop(b []byte) (resp []byte) {
|
||||
defer func() {
|
||||
if PanicHandler != nil {
|
||||
|
@ -6265,6 +6332,10 @@ func CommandAsync(cmd string, data []byte, callback func(data []byte)) {
|
|||
cd = WorkspaceExport(data)
|
||||
case "AccountRecover":
|
||||
cd = AccountRecover(data)
|
||||
case "AccountMigrate":
|
||||
cd = AccountMigrate(data)
|
||||
case "AccountMigrateCancel":
|
||||
cd = AccountMigrateCancel(data)
|
||||
case "AccountCreate":
|
||||
cd = AccountCreate(data)
|
||||
case "AccountDelete":
|
||||
|
@ -6275,6 +6346,8 @@ func CommandAsync(cmd string, data []byte, callback func(data []byte)) {
|
|||
cd = AccountSelect(data)
|
||||
case "AccountEnableLocalNetworkSync":
|
||||
cd = AccountEnableLocalNetworkSync(data)
|
||||
case "AccountChangeJsonApiAddr":
|
||||
cd = AccountChangeJsonApiAddr(data)
|
||||
case "AccountStop":
|
||||
cd = AccountStop(data)
|
||||
case "AccountMove":
|
||||
|
@ -7089,6 +7162,34 @@ func (h *ClientCommandsHandlerProxy) AccountRecover(ctx context.Context, req *pb
|
|||
call, _ := actualCall(ctx, req)
|
||||
return call.(*pb.RpcAccountRecoverResponse)
|
||||
}
|
||||
func (h *ClientCommandsHandlerProxy) AccountMigrate(ctx context.Context, req *pb.RpcAccountMigrateRequest) *pb.RpcAccountMigrateResponse {
|
||||
actualCall := func(ctx context.Context, req any) (any, error) {
|
||||
return h.client.AccountMigrate(ctx, req.(*pb.RpcAccountMigrateRequest)), nil
|
||||
}
|
||||
for _, interceptor := range h.interceptors {
|
||||
toCall := actualCall
|
||||
currentInterceptor := interceptor
|
||||
actualCall = func(ctx context.Context, req any) (any, error) {
|
||||
return currentInterceptor(ctx, req, "AccountMigrate", toCall)
|
||||
}
|
||||
}
|
||||
call, _ := actualCall(ctx, req)
|
||||
return call.(*pb.RpcAccountMigrateResponse)
|
||||
}
|
||||
func (h *ClientCommandsHandlerProxy) AccountMigrateCancel(ctx context.Context, req *pb.RpcAccountMigrateCancelRequest) *pb.RpcAccountMigrateCancelResponse {
|
||||
actualCall := func(ctx context.Context, req any) (any, error) {
|
||||
return h.client.AccountMigrateCancel(ctx, req.(*pb.RpcAccountMigrateCancelRequest)), nil
|
||||
}
|
||||
for _, interceptor := range h.interceptors {
|
||||
toCall := actualCall
|
||||
currentInterceptor := interceptor
|
||||
actualCall = func(ctx context.Context, req any) (any, error) {
|
||||
return currentInterceptor(ctx, req, "AccountMigrateCancel", toCall)
|
||||
}
|
||||
}
|
||||
call, _ := actualCall(ctx, req)
|
||||
return call.(*pb.RpcAccountMigrateCancelResponse)
|
||||
}
|
||||
func (h *ClientCommandsHandlerProxy) AccountCreate(ctx context.Context, req *pb.RpcAccountCreateRequest) *pb.RpcAccountCreateResponse {
|
||||
actualCall := func(ctx context.Context, req any) (any, error) {
|
||||
return h.client.AccountCreate(ctx, req.(*pb.RpcAccountCreateRequest)), nil
|
||||
|
@ -7159,6 +7260,20 @@ func (h *ClientCommandsHandlerProxy) AccountEnableLocalNetworkSync(ctx context.C
|
|||
call, _ := actualCall(ctx, req)
|
||||
return call.(*pb.RpcAccountEnableLocalNetworkSyncResponse)
|
||||
}
|
||||
func (h *ClientCommandsHandlerProxy) AccountChangeJsonApiAddr(ctx context.Context, req *pb.RpcAccountChangeJsonApiAddrRequest) *pb.RpcAccountChangeJsonApiAddrResponse {
|
||||
actualCall := func(ctx context.Context, req any) (any, error) {
|
||||
return h.client.AccountChangeJsonApiAddr(ctx, req.(*pb.RpcAccountChangeJsonApiAddrRequest)), nil
|
||||
}
|
||||
for _, interceptor := range h.interceptors {
|
||||
toCall := actualCall
|
||||
currentInterceptor := interceptor
|
||||
actualCall = func(ctx context.Context, req any) (any, error) {
|
||||
return currentInterceptor(ctx, req, "AccountChangeJsonApiAddr", toCall)
|
||||
}
|
||||
}
|
||||
call, _ := actualCall(ctx, req)
|
||||
return call.(*pb.RpcAccountChangeJsonApiAddrResponse)
|
||||
}
|
||||
func (h *ClientCommandsHandlerProxy) AccountStop(ctx context.Context, req *pb.RpcAccountStopRequest) *pb.RpcAccountStopResponse {
|
||||
actualCall := func(ctx context.Context, req any) (any, error) {
|
||||
return h.client.AccountStop(ctx, req.(*pb.RpcAccountStopRequest)), nil
|
||||
|
|
|
@ -9,16 +9,19 @@ import (
|
|||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-graphviz"
|
||||
"github.com/gogo/protobuf/jsonpb"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/state"
|
||||
"github.com/anyproto/anytype-heart/core/debug/treearchive"
|
||||
"github.com/anyproto/anytype-heart/core/debug/exporter"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
|
@ -42,22 +45,20 @@ func main() {
|
|||
return
|
||||
}
|
||||
fmt.Println("opening file...")
|
||||
st := time.Now()
|
||||
archive, err := treearchive.Open(*file)
|
||||
var (
|
||||
st = time.Now()
|
||||
ctx = context.Background()
|
||||
)
|
||||
res, err := exporter.ImportStorage(ctx, *file)
|
||||
if err != nil {
|
||||
log.Fatal("can't open debug file:", err)
|
||||
log.Fatal("can't import the tree:", err)
|
||||
}
|
||||
objectTree, err := res.CreateReadableTree(*fromRoot, "")
|
||||
if err != nil {
|
||||
log.Fatal("can't create readable tree:", err)
|
||||
}
|
||||
defer archive.Close()
|
||||
fmt.Printf("open archive done in %.1fs\n", time.Since(st).Seconds())
|
||||
|
||||
importer := treearchive.NewTreeImporter(archive.ListStorage(), archive.TreeStorage())
|
||||
st = time.Now()
|
||||
err = importer.Import(*fromRoot, "")
|
||||
if err != nil {
|
||||
log.Fatal("can't import the tree", err)
|
||||
}
|
||||
fmt.Printf("import tree done in %.1fs\n", time.Since(st).Seconds())
|
||||
|
||||
importer := exporter.NewTreeImporter(objectTree)
|
||||
if *makeJson {
|
||||
treeJson, err := importer.Json()
|
||||
if err != nil {
|
||||
|
@ -83,10 +84,11 @@ func main() {
|
|||
}
|
||||
fmt.Println("Change:")
|
||||
fmt.Println(pbtypes.Sprint(ch.Model))
|
||||
err = importer.Import(*fromRoot, ch.Id)
|
||||
objectTree, err = res.CreateReadableTree(*fromRoot, ch.Id)
|
||||
if err != nil {
|
||||
log.Fatal("can't import the tree before", ch.Id, err)
|
||||
log.Fatal("can't create readable tree:", err)
|
||||
}
|
||||
importer = exporter.NewTreeImporter(objectTree)
|
||||
}
|
||||
ot := importer.ObjectTree()
|
||||
di, err := ot.Debug(state.ChangeParser{})
|
||||
|
@ -126,12 +128,16 @@ func main() {
|
|||
|
||||
if *objectStore {
|
||||
fmt.Println("fetch object store info..")
|
||||
ls, err := archive.LocalStore()
|
||||
f, err := os.Open(filepath.Join(res.FolderPath, "localstore.json"))
|
||||
if err != nil {
|
||||
fmt.Println("can't open objectStore info:", err)
|
||||
} else {
|
||||
fmt.Println(pbtypes.Sprint(ls))
|
||||
log.Fatal("can't open objectStore info:", err)
|
||||
}
|
||||
defer f.Close()
|
||||
info := &model.ObjectInfo{}
|
||||
if err = jsonpb.Unmarshal(f, info); err != nil {
|
||||
log.Fatal("can't unmarshal objectStore info:", err)
|
||||
}
|
||||
fmt.Println(pbtypes.Sprint(info))
|
||||
}
|
||||
|
||||
if *makeTree {
|
||||
|
|
|
@ -31,6 +31,7 @@ import (
|
|||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core"
|
||||
"github.com/anyproto/anytype-heart/core/api"
|
||||
"github.com/anyproto/anytype-heart/core/event"
|
||||
"github.com/anyproto/anytype-heart/metrics"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
|
@ -225,6 +226,7 @@ func main() {
|
|||
}()
|
||||
|
||||
startReportMemory(mw)
|
||||
api.SetMiddlewareParams(mw)
|
||||
|
||||
shutdown := func() {
|
||||
server.Stop()
|
||||
|
|
|
@ -306,7 +306,7 @@ func getTableSizes(mw *core.Middleware) (tables map[string]uint64) {
|
|||
tables = make(map[string]uint64)
|
||||
cfg := mw.GetApp().MustComponent(config.CName).(*config.Config)
|
||||
|
||||
db, err := sql.Open("sqlite3", cfg.GetSpaceStorePath())
|
||||
db, err := sql.Open("sqlite3", cfg.GetSqliteStorePath())
|
||||
if err != nil {
|
||||
fmt.Println("Error opening database:", err)
|
||||
return
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/anyproto/anytype-heart/core/application"
|
||||
"github.com/anyproto/anytype-heart/core/session"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/space/spacecore/storage/migrator"
|
||||
)
|
||||
|
||||
func (mw *Middleware) AccountCreate(cctx context.Context, req *pb.RpcAccountCreateRequest) *pb.RpcAccountCreateResponse {
|
||||
|
@ -49,6 +50,35 @@ func (mw *Middleware) AccountRecover(cctx context.Context, _ *pb.RpcAccountRecov
|
|||
}
|
||||
}
|
||||
|
||||
func (mw *Middleware) AccountMigrate(cctx context.Context, req *pb.RpcAccountMigrateRequest) *pb.RpcAccountMigrateResponse {
|
||||
err := mw.applicationService.AccountMigrate(cctx, req)
|
||||
code := mapErrorCode(err,
|
||||
errToCode(application.ErrBadInput, pb.RpcAccountMigrateResponseError_BAD_INPUT),
|
||||
errToCode(application.ErrAccountNotFound, pb.RpcAccountMigrateResponseError_ACCOUNT_NOT_FOUND),
|
||||
errToCode(context.Canceled, pb.RpcAccountMigrateResponseError_CANCELED),
|
||||
errTypeToCode(&migrator.NotEnoughFreeSpaceError{}, pb.RpcAccountMigrateResponseError_NOT_ENOUGH_FREE_SPACE),
|
||||
)
|
||||
return &pb.RpcAccountMigrateResponse{
|
||||
Error: &pb.RpcAccountMigrateResponseError{
|
||||
Code: code,
|
||||
Description: getErrorDescription(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (mw *Middleware) AccountMigrateCancel(cctx context.Context, req *pb.RpcAccountMigrateCancelRequest) *pb.RpcAccountMigrateCancelResponse {
|
||||
err := mw.applicationService.AccountMigrateCancel(cctx, req)
|
||||
code := mapErrorCode(err,
|
||||
errToCode(application.ErrBadInput, pb.RpcAccountMigrateCancelResponseError_BAD_INPUT),
|
||||
)
|
||||
return &pb.RpcAccountMigrateCancelResponse{
|
||||
Error: &pb.RpcAccountMigrateCancelResponseError{
|
||||
Code: code,
|
||||
Description: getErrorDescription(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (mw *Middleware) AccountSelect(cctx context.Context, req *pb.RpcAccountSelectRequest) *pb.RpcAccountSelectResponse {
|
||||
account, err := mw.applicationService.AccountSelect(cctx, req)
|
||||
code := mapErrorCode(err,
|
||||
|
@ -64,6 +94,7 @@ func (mw *Middleware) AccountSelect(cctx context.Context, req *pb.RpcAccountSele
|
|||
errToCode(application.ErrAnotherProcessIsRunning, pb.RpcAccountSelectResponseError_ANOTHER_ANYTYPE_PROCESS_IS_RUNNING),
|
||||
errToCode(application.ErrIncompatibleVersion, pb.RpcAccountSelectResponseError_FAILED_TO_FETCH_REMOTE_NODE_HAS_INCOMPATIBLE_PROTO_VERSION),
|
||||
errToCode(application.ErrFailedToStartApplication, pb.RpcAccountSelectResponseError_FAILED_TO_RUN_NODE),
|
||||
errToCode(application.ErrAccountStoreIsNotMigrated, pb.RpcAccountSelectResponseError_ACCOUNT_STORE_NOT_MIGRATED),
|
||||
)
|
||||
return &pb.RpcAccountSelectResponse{
|
||||
Config: nil,
|
||||
|
@ -198,10 +229,23 @@ func (mw *Middleware) AccountEnableLocalNetworkSync(_ context.Context, req *pb.R
|
|||
}
|
||||
}
|
||||
|
||||
func (mw *Middleware) AccountChangeJsonApiAddr(ctx context.Context, req *pb.RpcAccountChangeJsonApiAddrRequest) *pb.RpcAccountChangeJsonApiAddrResponse {
|
||||
err := mw.applicationService.AccountChangeJsonApiAddr(ctx, req.ListenAddr)
|
||||
code := mapErrorCode(err,
|
||||
errToCode(application.ErrApplicationIsNotRunning, pb.RpcAccountChangeJsonApiAddrResponseError_ACCOUNT_IS_NOT_RUNNING),
|
||||
)
|
||||
return &pb.RpcAccountChangeJsonApiAddrResponse{
|
||||
Error: &pb.RpcAccountChangeJsonApiAddrResponseError{
|
||||
Code: code,
|
||||
Description: getErrorDescription(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (mw *Middleware) AccountLocalLinkNewChallenge(ctx context.Context, request *pb.RpcAccountLocalLinkNewChallengeRequest) *pb.RpcAccountLocalLinkNewChallengeResponse {
|
||||
info := getClientInfo(ctx)
|
||||
|
||||
challengeId, err := mw.applicationService.LinkLocalStartNewChallenge(&info)
|
||||
challengeId, err := mw.applicationService.LinkLocalStartNewChallenge(request.Scope, &info)
|
||||
code := mapErrorCode(err,
|
||||
errToCode(session.ErrTooManyChallengeRequests, pb.RpcAccountLocalLinkNewChallengeResponseError_TOO_MANY_REQUESTS),
|
||||
errToCode(application.ErrApplicationIsNotRunning, pb.RpcAccountLocalLinkNewChallengeResponseError_ACCOUNT_IS_NOT_RUNNING),
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/anyproto/any-sync/commonspace/acl/aclclient"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/list"
|
||||
"github.com/anyproto/any-sync/commonspace/object/acl/liststorage"
|
||||
"github.com/anyproto/any-sync/coordinator/coordinatorclient"
|
||||
"github.com/anyproto/any-sync/coordinator/coordinatorproto"
|
||||
"github.com/anyproto/any-sync/nodeconf"
|
||||
|
@ -392,7 +391,7 @@ func (a *aclService) ViewInvite(ctx context.Context, inviteCid cid.Cid, inviteFi
|
|||
if len(recs) == 0 {
|
||||
return domain.InviteView{}, fmt.Errorf("no acl records found for space: %s, %w", res.SpaceId, ErrAclRequestFailed)
|
||||
}
|
||||
store, err := liststorage.NewInMemoryAclListStorage(recs[0].Id, recs)
|
||||
store, err := list.NewInMemoryStorage(recs[0].Id, recs)
|
||||
if err != nil {
|
||||
return domain.InviteView{}, convertedOrAclRequestError(err)
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import (
|
|||
"github.com/anyproto/anytype-heart/core/acl"
|
||||
"github.com/anyproto/anytype-heart/core/anytype/account"
|
||||
"github.com/anyproto/anytype-heart/core/anytype/config"
|
||||
"github.com/anyproto/anytype-heart/core/api"
|
||||
"github.com/anyproto/anytype-heart/core/block"
|
||||
"github.com/anyproto/anytype-heart/core/block/backlinks"
|
||||
"github.com/anyproto/anytype-heart/core/block/bookmark"
|
||||
|
@ -45,7 +46,6 @@ import (
|
|||
"github.com/anyproto/anytype-heart/core/block/detailservice"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/converter"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
|
||||
"github.com/anyproto/anytype-heart/core/block/export"
|
||||
importer "github.com/anyproto/anytype-heart/core/block/import"
|
||||
"github.com/anyproto/anytype-heart/core/block/object/idderiver/idderiverimpl"
|
||||
|
@ -82,7 +82,6 @@ import (
|
|||
paymentscache "github.com/anyproto/anytype-heart/core/payments/cache"
|
||||
"github.com/anyproto/anytype-heart/core/peerstatus"
|
||||
"github.com/anyproto/anytype-heart/core/publish"
|
||||
"github.com/anyproto/anytype-heart/core/recordsbatcher"
|
||||
"github.com/anyproto/anytype-heart/core/session"
|
||||
"github.com/anyproto/anytype-heart/core/spaceview"
|
||||
"github.com/anyproto/anytype-heart/core/subscription"
|
||||
|
@ -109,9 +108,12 @@ import (
|
|||
"github.com/anyproto/anytype-heart/space/spacecore/clientserver"
|
||||
"github.com/anyproto/anytype-heart/space/spacecore/credentialprovider"
|
||||
"github.com/anyproto/anytype-heart/space/spacecore/localdiscovery"
|
||||
"github.com/anyproto/anytype-heart/space/spacecore/oldstorage"
|
||||
"github.com/anyproto/anytype-heart/space/spacecore/peermanager"
|
||||
"github.com/anyproto/anytype-heart/space/spacecore/peerstore"
|
||||
"github.com/anyproto/anytype-heart/space/spacecore/storage"
|
||||
"github.com/anyproto/anytype-heart/space/spacecore/storage/migrator"
|
||||
"github.com/anyproto/anytype-heart/space/spacecore/storage/migratorfinisher"
|
||||
"github.com/anyproto/anytype-heart/space/spacecore/typeprovider"
|
||||
"github.com/anyproto/anytype-heart/space/spacefactory"
|
||||
"github.com/anyproto/anytype-heart/space/virtualspaceservice"
|
||||
|
@ -207,6 +209,18 @@ func appVersion(a *app.App, clientWithVersion string) string {
|
|||
return clientWithVersion + "/middle:" + middleVersion + "/any-sync:" + anySyncVersion
|
||||
}
|
||||
|
||||
func BootstrapMigration(a *app.App, components ...app.Component) {
|
||||
for _, c := range components {
|
||||
a.Register(c)
|
||||
}
|
||||
a.Register(migratorfinisher.New()).
|
||||
Register(clientds.New()).
|
||||
Register(oldstorage.New()).
|
||||
Register(storage.New()).
|
||||
Register(process.New()).
|
||||
Register(migrator.New())
|
||||
}
|
||||
|
||||
func Bootstrap(a *app.App, components ...app.Component) {
|
||||
for _, c := range components {
|
||||
a.Register(c)
|
||||
|
@ -278,7 +292,6 @@ func Bootstrap(a *app.App, components ...app.Component) {
|
|||
Register(acl.New()).
|
||||
Register(builtintemplate.New()).
|
||||
Register(converter.NewLayoutConverter()).
|
||||
Register(recordsbatcher.New()).
|
||||
Register(configfetcher.New()).
|
||||
Register(process.New()).
|
||||
Register(core.NewTempDirService()).
|
||||
|
@ -320,8 +333,8 @@ func Bootstrap(a *app.App, components ...app.Component) {
|
|||
Register(payments.New()).
|
||||
Register(paymentscache.New()).
|
||||
Register(peerstatus.New()).
|
||||
Register(lastused.New()).
|
||||
Register(spaceview.New())
|
||||
Register(spaceview.New()).
|
||||
Register(api.New())
|
||||
}
|
||||
|
||||
func MiddlewareVersion() string {
|
||||
|
|
|
@ -43,6 +43,7 @@ const (
|
|||
const (
|
||||
SpaceStoreBadgerPath = "spacestore"
|
||||
SpaceStoreSqlitePath = "spaceStore.db"
|
||||
SpaceStoreNewPath = "spaceStoreNew"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -69,11 +70,13 @@ type Config struct {
|
|||
DisableThreadsSyncEvents bool
|
||||
DontStartLocalNetworkSyncAutomatically bool
|
||||
PeferYamuxTransport bool
|
||||
DisableNetworkIdCheck bool
|
||||
SpaceStorageMode storage.SpaceStorageMode
|
||||
NetworkMode pb.RpcAccountNetworkMode
|
||||
NetworkCustomConfigFilePath string `json:",omitempty"` // not saved to config
|
||||
SqliteTempPath string `json:",omitempty"` // not saved to config
|
||||
AnyStoreConfig *anystore.Config `json:",omitempty"` // not saved to config
|
||||
JsonApiListenAddr string `json:",omitempty"` // empty means disabled
|
||||
|
||||
RepoPath string
|
||||
AnalyticsId string
|
||||
|
@ -292,12 +295,27 @@ func (c *Config) FSConfig() (FSConfig, error) {
|
|||
return FSConfig{IPFSStorageAddr: res.CustomFileStorePath}, nil
|
||||
}
|
||||
|
||||
func (c *Config) GetRepoPath() string {
|
||||
return c.RepoPath
|
||||
}
|
||||
|
||||
func (c *Config) GetConfigPath() string {
|
||||
return filepath.Join(c.RepoPath, ConfigFileName)
|
||||
}
|
||||
|
||||
func (c *Config) GetSpaceStorePath() string {
|
||||
return filepath.Join(c.RepoPath, "spaceStore.db")
|
||||
func (c *Config) GetSqliteStorePath() string {
|
||||
return filepath.Join(c.RepoPath, SpaceStoreSqlitePath)
|
||||
}
|
||||
|
||||
func (c *Config) GetOldSpaceStorePath() string {
|
||||
if c.GetSpaceStorageMode() == storage.SpaceStorageModeBadger {
|
||||
return filepath.Join(c.RepoPath, SpaceStoreBadgerPath)
|
||||
}
|
||||
return c.GetSqliteStorePath()
|
||||
}
|
||||
|
||||
func (c *Config) GetNewSpaceStorePath() string {
|
||||
return filepath.Join(c.RepoPath, SpaceStoreNewPath)
|
||||
}
|
||||
|
||||
func (c *Config) GetTempDirPath() string {
|
||||
|
@ -390,7 +408,7 @@ func (c *Config) GetNodeConfWithError() (conf nodeconf.Configuration, err error)
|
|||
if err := yaml.Unmarshal(confBytes, &conf); err != nil {
|
||||
return nodeconf.Configuration{}, errors.Join(ErrNetworkFileFailedToRead, err)
|
||||
}
|
||||
if c.NetworkId != "" && c.NetworkId != conf.NetworkId {
|
||||
if !c.DisableNetworkIdCheck && c.NetworkId != "" && c.NetworkId != conf.NetworkId {
|
||||
log.Warnf("Network id mismatch: %s != %s", c.NetworkId, conf.NetworkId)
|
||||
return nodeconf.Configuration{}, errors.Join(ErrNetworkIdMismatch, fmt.Errorf("network id mismatch: %s != %s", c.NetworkId, conf.NetworkId))
|
||||
}
|
||||
|
|
1638
core/api/docs/docs.go
Normal file
1638
core/api/docs/docs.go
Normal file
File diff suppressed because it is too large
Load diff
1614
core/api/docs/swagger.json
Normal file
1614
core/api/docs/swagger.json
Normal file
File diff suppressed because it is too large
Load diff
1108
core/api/docs/swagger.yaml
Normal file
1108
core/api/docs/swagger.yaml
Normal file
File diff suppressed because it is too large
Load diff
75
core/api/internal/auth/handler.go
Normal file
75
core/api/internal/auth/handler.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/api/util"
|
||||
)
|
||||
|
||||
// DisplayCodeHandler starts a new challenge and returns the challenge ID
|
||||
//
|
||||
// @Summary Start new challenge
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param app_name query string true "App name requesting the challenge"
|
||||
// @Success 200 {object} DisplayCodeResponse "Challenge ID"
|
||||
// @Failure 400 {object} util.ValidationError "Invalid input"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /auth/display_code [post]
|
||||
func DisplayCodeHandler(s *AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
appName := c.Query("app_name")
|
||||
|
||||
challengeId, err := s.NewChallenge(c.Request.Context(), appName)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrMissingAppName, http.StatusBadRequest),
|
||||
util.ErrToCode(ErrFailedGenerateChallenge, http.StatusInternalServerError))
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, DisplayCodeResponse{ChallengeId: challengeId})
|
||||
}
|
||||
}
|
||||
|
||||
// TokenHandler retrieves an authentication token using a code and challenge ID
|
||||
//
|
||||
// @Summary Retrieve token
|
||||
// @Tags auth
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param challenge_id query string true "Challenge ID"
|
||||
// @Param code query string true "4-digit code retrieved from Anytype Desktop app"
|
||||
// @Success 200 {object} TokenResponse "Authentication token"
|
||||
// @Failure 400 {object} util.ValidationError "Invalid input"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /auth/token [post]
|
||||
func TokenHandler(s *AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
challengeId := c.Query("challenge_id")
|
||||
code := c.Query("code")
|
||||
|
||||
sessionToken, appKey, err := s.SolveChallenge(c.Request.Context(), challengeId, code)
|
||||
errCode := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrInvalidInput, http.StatusBadRequest),
|
||||
util.ErrToCode(ErrFailedAuthenticate, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if errCode != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(errCode, err.Error())
|
||||
c.JSON(errCode, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, TokenResponse{
|
||||
SessionToken: sessionToken,
|
||||
AppKey: appKey,
|
||||
})
|
||||
}
|
||||
}
|
10
core/api/internal/auth/model.go
Normal file
10
core/api/internal/auth/model.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package auth
|
||||
|
||||
type DisplayCodeResponse struct {
|
||||
ChallengeId string `json:"challenge_id" example:"67647f5ecda913e9a2e11b26"`
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
SessionToken string `json:"session_token" example:"eyJhbGciOeJIRzI1NiIsInR5cCI6IkpXVCJ1.eyJzZWVkIjaiY0dmVndlUnAifQ.Y1EZecYnwmvMkrXKOa2XJnAbaRt34urBabe06tmDQII"`
|
||||
AppKey string `json:"app_key" example:"zhSG/zQRmgADyilWPtgdnfo1qD60oK02/SVgi1GaFt6="`
|
||||
}
|
67
core/api/internal/auth/service.go
Normal file
67
core/api/internal/auth/service.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingAppName = errors.New("missing app name")
|
||||
ErrFailedGenerateChallenge = errors.New("failed to generate a new challenge")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrFailedAuthenticate = errors.New("failed to authenticate user")
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
NewChallenge(ctx context.Context, appName string) (string, error)
|
||||
SolveChallenge(ctx context.Context, challengeId string, code string) (sessionToken, appKey string, err error)
|
||||
}
|
||||
|
||||
type AuthService struct {
|
||||
mw service.ClientCommandsServer
|
||||
}
|
||||
|
||||
func NewService(mw service.ClientCommandsServer) *AuthService {
|
||||
return &AuthService{mw: mw}
|
||||
}
|
||||
|
||||
// NewChallenge calls AccountLocalLinkNewChallenge and returns the challenge ID, or an error if it fails.
|
||||
func (s *AuthService) NewChallenge(ctx context.Context, appName string) (string, error) {
|
||||
if appName == "" {
|
||||
return "", ErrMissingAppName
|
||||
}
|
||||
|
||||
resp := s.mw.AccountLocalLinkNewChallenge(ctx, &pb.RpcAccountLocalLinkNewChallengeRequest{
|
||||
AppName: appName,
|
||||
Scope: model.AccountAuth_JsonAPI,
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcAccountLocalLinkNewChallengeResponseError_NULL {
|
||||
return "", ErrFailedGenerateChallenge
|
||||
}
|
||||
|
||||
return resp.ChallengeId, nil
|
||||
}
|
||||
|
||||
// SolveChallenge calls AccountLocalLinkSolveChallenge and returns the session token + app key, or an error if it fails.
|
||||
func (s *AuthService) SolveChallenge(ctx context.Context, challengeId string, code string) (sessionToken string, appKey string, err error) {
|
||||
if challengeId == "" || code == "" {
|
||||
return "", "", ErrInvalidInput
|
||||
}
|
||||
|
||||
// Call AccountLocalLinkSolveChallenge to retrieve session token and app key
|
||||
resp := s.mw.AccountLocalLinkSolveChallenge(ctx, &pb.RpcAccountLocalLinkSolveChallengeRequest{
|
||||
ChallengeId: challengeId,
|
||||
Answer: code,
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcAccountLocalLinkSolveChallengeResponseError_NULL {
|
||||
return "", "", ErrFailedAuthenticate
|
||||
}
|
||||
|
||||
return resp.SessionToken, resp.AppKey, nil
|
||||
}
|
161
core/api/internal/auth/service_test.go
Normal file
161
core/api/internal/auth/service_test.go
Normal file
|
@ -0,0 +1,161 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service/mock_service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
)
|
||||
|
||||
const (
|
||||
mockedAppName = "api-test"
|
||||
mockedChallengeId = "mocked-challenge-id"
|
||||
mockedCode = "mocked-mockedCode"
|
||||
mockedSessionToken = "mocked-session-token"
|
||||
mockedAppKey = "mocked-app-key"
|
||||
)
|
||||
|
||||
type fixture struct {
|
||||
*AuthService
|
||||
mwMock *mock_service.MockClientCommandsServer
|
||||
}
|
||||
|
||||
func newFixture(t *testing.T) *fixture {
|
||||
mw := mock_service.NewMockClientCommandsServer(t)
|
||||
authService := NewService(mw)
|
||||
|
||||
return &fixture{
|
||||
AuthService: authService,
|
||||
mwMock: mw,
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthService_GenerateNewChallenge(t *testing.T) {
|
||||
t.Run("successful challenge creation", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("AccountLocalLinkNewChallenge", mock.Anything, &pb.RpcAccountLocalLinkNewChallengeRequest{
|
||||
AppName: mockedAppName,
|
||||
Scope: model.AccountAuth_JsonAPI,
|
||||
}).
|
||||
Return(&pb.RpcAccountLocalLinkNewChallengeResponse{
|
||||
ChallengeId: mockedChallengeId,
|
||||
Error: &pb.RpcAccountLocalLinkNewChallengeResponseError{Code: pb.RpcAccountLocalLinkNewChallengeResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
challengeId, err := fx.NewChallenge(ctx, mockedAppName)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockedChallengeId, challengeId)
|
||||
})
|
||||
|
||||
t.Run("bad request - missing app name", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
// when
|
||||
challengeId, err := fx.NewChallenge(ctx, "")
|
||||
|
||||
// then
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ErrMissingAppName, err)
|
||||
require.Empty(t, challengeId)
|
||||
})
|
||||
|
||||
t.Run("failed challenge creation", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("AccountLocalLinkNewChallenge", mock.Anything, &pb.RpcAccountLocalLinkNewChallengeRequest{
|
||||
AppName: mockedAppName,
|
||||
Scope: model.AccountAuth_JsonAPI,
|
||||
}).
|
||||
Return(&pb.RpcAccountLocalLinkNewChallengeResponse{
|
||||
Error: &pb.RpcAccountLocalLinkNewChallengeResponseError{Code: pb.RpcAccountLocalLinkNewChallengeResponseError_UNKNOWN_ERROR},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
challengeId, err := fx.NewChallenge(ctx, mockedAppName)
|
||||
|
||||
// then
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ErrFailedGenerateChallenge, err)
|
||||
require.Empty(t, challengeId)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthService_SolveChallengeForToken(t *testing.T) {
|
||||
t.Run("successful token retrieval", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("AccountLocalLinkSolveChallenge", mock.Anything, &pb.RpcAccountLocalLinkSolveChallengeRequest{
|
||||
ChallengeId: mockedChallengeId,
|
||||
Answer: mockedCode,
|
||||
}).
|
||||
Return(&pb.RpcAccountLocalLinkSolveChallengeResponse{
|
||||
SessionToken: mockedSessionToken,
|
||||
AppKey: mockedAppKey,
|
||||
Error: &pb.RpcAccountLocalLinkSolveChallengeResponseError{Code: pb.RpcAccountLocalLinkSolveChallengeResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
sessionToken, appKey, err := fx.SolveChallenge(ctx, mockedChallengeId, mockedCode)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockedSessionToken, sessionToken)
|
||||
require.Equal(t, mockedAppKey, appKey)
|
||||
|
||||
})
|
||||
|
||||
t.Run("bad request - missing challenge id or code", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
// when
|
||||
sessionToken, appKey, err := fx.SolveChallenge(ctx, "", "")
|
||||
|
||||
// then
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ErrInvalidInput, err)
|
||||
require.Empty(t, sessionToken)
|
||||
require.Empty(t, appKey)
|
||||
})
|
||||
|
||||
t.Run("failed token retrieval", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("AccountLocalLinkSolveChallenge", mock.Anything, &pb.RpcAccountLocalLinkSolveChallengeRequest{
|
||||
ChallengeId: mockedChallengeId,
|
||||
Answer: mockedCode,
|
||||
}).
|
||||
Return(&pb.RpcAccountLocalLinkSolveChallengeResponse{
|
||||
Error: &pb.RpcAccountLocalLinkSolveChallengeResponseError{Code: pb.RpcAccountLocalLinkSolveChallengeResponseError_UNKNOWN_ERROR},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
sessionToken, appKey, err := fx.SolveChallenge(ctx, mockedChallengeId, mockedCode)
|
||||
|
||||
// then
|
||||
require.Error(t, err)
|
||||
require.Equal(t, ErrFailedAuthenticate, err)
|
||||
require.Empty(t, sessionToken)
|
||||
require.Empty(t, appKey)
|
||||
})
|
||||
}
|
49
core/api/internal/export/handler.go
Normal file
49
core/api/internal/export/handler.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package export
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/api/util"
|
||||
)
|
||||
|
||||
// GetObjectExportHandler exports an object in specified format
|
||||
//
|
||||
// @Summary Export object
|
||||
// @Tags export
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "Space ID"
|
||||
// @Param object_id path string true "Object ID"
|
||||
// @Param format path string true "Export format" Enums(markdown,protobuf)
|
||||
// @Success 200 {object} ObjectExportResponse "Object exported successfully"
|
||||
// @Failure 400 {object} util.ValidationError "Bad request"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects/{object_id}/export/{format} [post]
|
||||
func GetObjectExportHandler(s *ExportService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
objectId := c.Param("object_id")
|
||||
format := c.Query("format")
|
||||
|
||||
objectAsRequest := ObjectExportRequest{}
|
||||
if err := c.ShouldBindJSON(&objectAsRequest); err != nil {
|
||||
apiErr := util.CodeToAPIError(http.StatusBadRequest, ErrBadInput.Error())
|
||||
c.JSON(http.StatusBadRequest, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
outputPath, err := s.GetObjectExport(c.Request.Context(), spaceId, objectId, format, objectAsRequest.Path)
|
||||
code := util.MapErrorCode(err, util.ErrToCode(ErrFailedExportObjectAsMarkdown, http.StatusInternalServerError))
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ObjectExportResponse{Path: outputPath})
|
||||
}
|
||||
}
|
9
core/api/internal/export/model.go
Normal file
9
core/api/internal/export/model.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package export
|
||||
|
||||
type ObjectExportRequest struct {
|
||||
Path string `json:"path" example:"/path/to/export"`
|
||||
}
|
||||
|
||||
type ObjectExportResponse struct {
|
||||
Path string `json:"path" example:"/path/to/export"`
|
||||
}
|
62
core/api/internal/export/service.go
Normal file
62
core/api/internal/export/service.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package export
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailedExportObjectAsMarkdown = errors.New("failed to export object as markdown")
|
||||
ErrBadInput = errors.New("bad input")
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetObjectExport(ctx context.Context, spaceId string, objectId string, format string, path string) (string, error)
|
||||
}
|
||||
|
||||
type ExportService struct {
|
||||
mw service.ClientCommandsServer
|
||||
AccountInfo *model.AccountInfo
|
||||
}
|
||||
|
||||
func NewService(mw service.ClientCommandsServer) *ExportService {
|
||||
return &ExportService{mw: mw}
|
||||
}
|
||||
|
||||
// GetObjectExport retrieves an object from a space and exports it as a specific format.
|
||||
func (s *ExportService) GetObjectExport(ctx context.Context, spaceId string, objectId string, format string, path string) (string, error) {
|
||||
resp := s.mw.ObjectListExport(ctx, &pb.RpcObjectListExportRequest{
|
||||
SpaceId: spaceId,
|
||||
Path: path,
|
||||
ObjectIds: []string{objectId},
|
||||
Format: s.mapStringToFormat(format),
|
||||
Zip: false,
|
||||
IncludeNested: false,
|
||||
IncludeFiles: true,
|
||||
IsJson: false,
|
||||
IncludeArchived: false,
|
||||
NoProgress: true,
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectListExportResponseError_NULL {
|
||||
return "", ErrFailedExportObjectAsMarkdown
|
||||
}
|
||||
|
||||
return resp.Path, nil
|
||||
}
|
||||
|
||||
// mapStringToFormat maps a format string to an ExportFormat enum.
|
||||
func (s *ExportService) mapStringToFormat(format string) model.ExportFormat {
|
||||
switch format {
|
||||
case "markdown":
|
||||
return model.Export_Markdown
|
||||
case "protobuf":
|
||||
return model.Export_Protobuf
|
||||
default:
|
||||
return model.Export_Markdown
|
||||
}
|
||||
}
|
134
core/api/internal/export/service_test.go
Normal file
134
core/api/internal/export/service_test.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package export
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service/mock_service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
)
|
||||
|
||||
const (
|
||||
spaceID = "space-123"
|
||||
objectID = "obj-456"
|
||||
exportFormat = "markdown"
|
||||
unrecognizedFormat = "unrecognized"
|
||||
exportPath = "/some/dir/myexport"
|
||||
)
|
||||
|
||||
type fixture struct {
|
||||
*ExportService
|
||||
mwMock *mock_service.MockClientCommandsServer
|
||||
}
|
||||
|
||||
func newFixture(t *testing.T) *fixture {
|
||||
mw := mock_service.NewMockClientCommandsServer(t)
|
||||
exportService := NewService(mw)
|
||||
|
||||
return &fixture{
|
||||
ExportService: exportService,
|
||||
mwMock: mw,
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportService_GetObjectExport(t *testing.T) {
|
||||
t.Run("successful export to markdown", func(t *testing.T) {
|
||||
// Given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
// Mock the ObjectListExport call
|
||||
fx.mwMock.
|
||||
On("ObjectListExport", mock.Anything, &pb.RpcObjectListExportRequest{
|
||||
SpaceId: spaceID,
|
||||
Path: exportPath,
|
||||
ObjectIds: []string{objectID},
|
||||
Format: model.Export_Markdown,
|
||||
Zip: false,
|
||||
IncludeNested: false,
|
||||
IncludeFiles: true,
|
||||
IsJson: false,
|
||||
IncludeArchived: false,
|
||||
NoProgress: true,
|
||||
}).
|
||||
Return(&pb.RpcObjectListExportResponse{
|
||||
Path: exportPath,
|
||||
Error: &pb.RpcObjectListExportResponseError{
|
||||
Code: pb.RpcObjectListExportResponseError_NULL,
|
||||
},
|
||||
}).
|
||||
Once()
|
||||
|
||||
// When
|
||||
gotPath, err := fx.GetObjectExport(ctx, spaceID, objectID, exportFormat, exportPath)
|
||||
|
||||
// Then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, exportPath, gotPath)
|
||||
fx.mwMock.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("failed export returns error", func(t *testing.T) {
|
||||
// Given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
// Mock the ObjectListExport call to return an error code
|
||||
fx.mwMock.
|
||||
On("ObjectListExport", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcObjectListExportResponse{
|
||||
Path: "",
|
||||
Error: &pb.RpcObjectListExportResponseError{
|
||||
Code: pb.RpcObjectListExportResponseError_UNKNOWN_ERROR,
|
||||
},
|
||||
}).
|
||||
Once()
|
||||
|
||||
// When
|
||||
gotPath, err := fx.GetObjectExport(ctx, spaceID, objectID, exportFormat, exportPath)
|
||||
|
||||
// Then
|
||||
require.Error(t, err)
|
||||
require.Empty(t, gotPath)
|
||||
require.ErrorIs(t, err, ErrFailedExportObjectAsMarkdown)
|
||||
fx.mwMock.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("unrecognized format defaults to markdown", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.
|
||||
On("ObjectListExport", mock.Anything, &pb.RpcObjectListExportRequest{
|
||||
SpaceId: spaceID,
|
||||
Path: exportPath,
|
||||
ObjectIds: []string{objectID},
|
||||
Format: model.Export_Markdown, // fallback
|
||||
Zip: false,
|
||||
IncludeNested: false,
|
||||
IncludeFiles: true,
|
||||
IsJson: false,
|
||||
IncludeArchived: false,
|
||||
NoProgress: true,
|
||||
}).
|
||||
Return(&pb.RpcObjectListExportResponse{
|
||||
Path: exportPath,
|
||||
Error: &pb.RpcObjectListExportResponseError{
|
||||
Code: pb.RpcObjectListExportResponseError_NULL,
|
||||
},
|
||||
}).
|
||||
Once()
|
||||
|
||||
// When
|
||||
gotPath, err := fx.GetObjectExport(ctx, spaceID, objectID, unrecognizedFormat, exportPath) //
|
||||
|
||||
// Then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, exportPath, gotPath)
|
||||
fx.mwMock.AssertExpectations(t)
|
||||
})
|
||||
}
|
303
core/api/internal/object/handler.go
Normal file
303
core/api/internal/object/handler.go
Normal file
|
@ -0,0 +1,303 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/core/api/util"
|
||||
)
|
||||
|
||||
// GetObjectsHandler retrieves a list of objects in a space
|
||||
//
|
||||
// @Summary List objects
|
||||
// @Tags objects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "Space ID"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
|
||||
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
|
||||
// @Success 200 {object} pagination.PaginatedResponse[Object] "List of objects"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects [get]
|
||||
func GetObjectsHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
objects, total, hasMore, err := s.ListObjects(c.Request.Context(), spaceId, offset, limit)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrFailedRetrieveObjects, http.StatusInternalServerError),
|
||||
util.ErrToCode(ErrObjectNotFound, http.StatusInternalServerError),
|
||||
util.ErrToCode(ErrFailedRetrieveObject, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, objects, total, offset, limit, hasMore)
|
||||
}
|
||||
}
|
||||
|
||||
// GetObjectHandler retrieves an object in a space
|
||||
//
|
||||
// @Summary Get object
|
||||
// @Tags objects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "Space ID"
|
||||
// @Param object_id path string true "Object ID"
|
||||
// @Success 200 {object} ObjectResponse "The requested object"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects/{object_id} [get]
|
||||
func GetObjectHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
objectId := c.Param("object_id")
|
||||
|
||||
object, err := s.GetObject(c.Request.Context(), spaceId, objectId)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrObjectNotFound, http.StatusNotFound),
|
||||
util.ErrToCode(ErrFailedRetrieveObject, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ObjectResponse{Object: object})
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteObjectHandler deletes an object in a space
|
||||
//
|
||||
// @Summary Delete object
|
||||
// @Tags objects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "Space ID"
|
||||
// @Param object_id path string true "Object ID"
|
||||
// @Success 200 {object} ObjectResponse "The deleted object"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 403 {object} util.ForbiddenError "Forbidden"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects/{object_id} [delete]
|
||||
func DeleteObjectHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
objectId := c.Param("object_id")
|
||||
|
||||
object, err := s.DeleteObject(c.Request.Context(), spaceId, objectId)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrObjectNotFound, http.StatusNotFound),
|
||||
util.ErrToCode(ErrFailedDeleteObject, http.StatusForbidden),
|
||||
util.ErrToCode(ErrFailedRetrieveObject, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ObjectResponse{Object: object})
|
||||
}
|
||||
}
|
||||
|
||||
// CreateObjectHandler creates a new object in a space
|
||||
//
|
||||
// @Summary Create object
|
||||
// @Tags objects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "Space ID"
|
||||
// @Param object body CreateObjectRequest true "Object to create"
|
||||
// @Success 200 {object} ObjectResponse "The created object"
|
||||
// @Failure 400 {object} util.ValidationError "Bad request"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/objects [post]
|
||||
func CreateObjectHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
|
||||
request := CreateObjectRequest{}
|
||||
if err := c.BindJSON(&request); err != nil {
|
||||
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
|
||||
c.JSON(http.StatusBadRequest, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
object, err := s.CreateObject(c.Request.Context(), spaceId, request)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrInputMissingSource, http.StatusBadRequest),
|
||||
util.ErrToCode(ErrFailedCreateObject, http.StatusInternalServerError),
|
||||
util.ErrToCode(ErrFailedSetRelationFeatured, http.StatusInternalServerError),
|
||||
util.ErrToCode(ErrFailedFetchBookmark, http.StatusInternalServerError),
|
||||
util.ErrToCode(ErrObjectNotFound, http.StatusInternalServerError),
|
||||
util.ErrToCode(ErrFailedRetrieveObject, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, ObjectResponse{Object: object})
|
||||
}
|
||||
}
|
||||
|
||||
// GetTypesHandler retrieves a list of types in a space
|
||||
//
|
||||
// @Summary List types
|
||||
// @Tags types
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "Space ID"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
|
||||
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
|
||||
// @Success 200 {object} pagination.PaginatedResponse[Type] "List of types"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/types [get]
|
||||
func GetTypesHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
types, total, hasMore, err := s.ListTypes(c.Request.Context(), spaceId, offset, limit)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrFailedRetrieveTypes, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, types, total, offset, limit, hasMore)
|
||||
}
|
||||
}
|
||||
|
||||
// GetTypeHandler retrieves a type in a space
|
||||
//
|
||||
// @Summary Get type
|
||||
// @Tags types
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "Space ID"
|
||||
// @Param type_id path string true "Type ID"
|
||||
// @Success 200 {object} TypeResponse "The requested type"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/types/{type_id} [get]
|
||||
func GetTypeHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
typeId := c.Param("type_id")
|
||||
|
||||
object, err := s.GetType(c.Request.Context(), spaceId, typeId)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrTypeNotFound, http.StatusNotFound),
|
||||
util.ErrToCode(ErrFailedRetrieveType, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, TypeResponse{Type: object})
|
||||
}
|
||||
}
|
||||
|
||||
// GetTemplatesHandler retrieves a list of templates for a type in a space
|
||||
//
|
||||
// @Summary List templates
|
||||
// @Tags types
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "Space ID"
|
||||
// @Param type_id path string true "Type ID"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
|
||||
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
|
||||
// @Success 200 {object} pagination.PaginatedResponse[Template] "List of templates"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/types/{type_id}/templates [get]
|
||||
func GetTemplatesHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
typeId := c.Param("type_id")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
templates, total, hasMore, err := s.ListTemplates(c.Request.Context(), spaceId, typeId, offset, limit)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrFailedRetrieveTemplateType, http.StatusInternalServerError),
|
||||
util.ErrToCode(ErrTemplateTypeNotFound, http.StatusInternalServerError),
|
||||
util.ErrToCode(ErrFailedRetrieveTemplates, http.StatusInternalServerError),
|
||||
util.ErrToCode(ErrFailedRetrieveTemplate, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, templates, total, offset, limit, hasMore)
|
||||
}
|
||||
}
|
||||
|
||||
// GetTemplateHandler retrieves a template for a type in a space
|
||||
//
|
||||
// @Summary Get template
|
||||
// @Tags types
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "Space ID"
|
||||
// @Param type_id path string true "Type ID"
|
||||
// @Param template_id path string true "Template ID"
|
||||
// @Success 200 {object} TemplateResponse "The requested template"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 404 {object} util.NotFoundError "Resource not found"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/types/{type_id}/templates/{template_id} [get]
|
||||
func GetTemplateHandler(s *ObjectService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
typeId := c.Param("type_id")
|
||||
templateId := c.Param("template_id")
|
||||
|
||||
object, err := s.GetTemplate(c.Request.Context(), spaceId, typeId, templateId)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrTemplateNotFound, http.StatusNotFound),
|
||||
util.ErrToCode(ErrFailedRetrieveTemplate, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, TemplateResponse{Template: object})
|
||||
}
|
||||
}
|
93
core/api/internal/object/model.go
Normal file
93
core/api/internal/object/model.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package object
|
||||
|
||||
type CreateObjectRequest struct {
|
||||
Name string `json:"name" example:"Object Name"`
|
||||
Icon string `json:"icon" example:"📄"`
|
||||
Description string `json:"description" example:"Object Description"`
|
||||
Body string `json:"body" example:"Object Body"`
|
||||
Source string `json:"source" example:"https://source.com"`
|
||||
TemplateId string `json:"template_id" example:"bafyreictrp3obmnf6dwejy5o4p7bderaaia4bdg2psxbfzf44yya5uutge"`
|
||||
ObjectTypeUniqueKey string `json:"object_type_unique_key" example:"ot-page"`
|
||||
}
|
||||
|
||||
type ObjectResponse struct {
|
||||
Object Object `json:"object"`
|
||||
}
|
||||
|
||||
type Object struct {
|
||||
Type string `json:"type" example:"Page"`
|
||||
Id string `json:"id" example:"bafyreie6n5l5nkbjal37su54cha4coy7qzuhrnajluzv5qd5jvtsrxkequ"`
|
||||
Name string `json:"name" example:"Object Name"`
|
||||
Icon string `json:"icon" example:"📄"`
|
||||
Snippet string `json:"snippet" example:"The beginning of the object body..."`
|
||||
Layout string `json:"layout" example:"basic"`
|
||||
SpaceId string `json:"space_id" example:"bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1"`
|
||||
RootId string `json:"root_id" example:"bafyreicypzj6uvu54664ucv3hmbsd5cmdy2dv4fwua26sciq74khzpyn4u"`
|
||||
Blocks []Block `json:"blocks"`
|
||||
Details []Detail `json:"details"`
|
||||
}
|
||||
|
||||
type Block struct {
|
||||
Id string `json:"id" example:"64394517de52ad5acb89c66c"`
|
||||
ChildrenIds []string `json:"children_ids" example:"['6797ce8ecda913cde14b02dc']"`
|
||||
BackgroundColor string `json:"background_color" example:"red"`
|
||||
Align string `json:"align" enums:"AlignLeft,AlignCenter,AlignRight,AlignJustify" example:"AlignLeft"`
|
||||
VerticalAlign string `json:"vertical_align" enums:"VerticalAlignTop,VerticalAlignMiddle,VerticalAlignBottom" example:"VerticalAlignTop"`
|
||||
Text *Text `json:"text,omitempty"`
|
||||
File *File `json:"file,omitempty"`
|
||||
}
|
||||
|
||||
type Text struct {
|
||||
Text string `json:"text" example:"Some text"`
|
||||
Style string `json:"style" enums:"Paragraph,Header1,Header2,Header3,Header4,Quote,Code,Title,Checkbox,Marked,Numbered,Toggle,Description,Callout" example:"Paragraph"`
|
||||
Checked bool `json:"checked" example:"true"`
|
||||
Color string `json:"color" example:"red"`
|
||||
Icon string `json:"icon" example:"📄"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
Hash string `json:"hash"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Mime string `json:"mime"`
|
||||
Size int `json:"size"`
|
||||
AddedAt int `json:"added_at"`
|
||||
TargetObjectId string `json:"target_object_id"`
|
||||
State string `json:"state"`
|
||||
Style string `json:"style"`
|
||||
}
|
||||
|
||||
type Detail struct {
|
||||
Id string `json:"id" enums:"last_modified_date,last_modified_by,created_date,created_by,last_opened_date,tags" example:"last_modified_date"`
|
||||
Details map[string]interface{} `json:"details"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Id string `json:"id" example:"bafyreiaixlnaefu3ci22zdenjhsdlyaeeoyjrsid5qhfeejzlccijbj7sq"`
|
||||
Name string `json:"name" example:"Tag Name"`
|
||||
Color string `json:"color" example:"yellow"`
|
||||
}
|
||||
|
||||
type TypeResponse struct {
|
||||
Type Type `json:"type"`
|
||||
}
|
||||
|
||||
type Type struct {
|
||||
Type string `json:"type" example:"type"`
|
||||
Id string `json:"id" example:"bafyreigyb6l5szohs32ts26ku2j42yd65e6hqy2u3gtzgdwqv6hzftsetu"`
|
||||
UniqueKey string `json:"unique_key" example:"ot-page"`
|
||||
Name string `json:"name" example:"Page"`
|
||||
Icon string `json:"icon" example:"📄"`
|
||||
RecommendedLayout string `json:"recommended_layout" example:"todo"`
|
||||
}
|
||||
|
||||
type TemplateResponse struct {
|
||||
Template Template `json:"template"`
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
Type string `json:"type" example:"template"`
|
||||
Id string `json:"id" example:"bafyreictrp3obmnf6dwejy5o4p7bderaaia4bdg2psxbfzf44yya5uutge"`
|
||||
Name string `json:"name" example:"Template Name"`
|
||||
Icon string `json:"icon" example:"📄"`
|
||||
}
|
564
core/api/internal/object/service.go
Normal file
564
core/api/internal/object/service.go
Normal file
|
@ -0,0 +1,564 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/space"
|
||||
"github.com/anyproto/anytype-heart/core/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/core/api/util"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
var (
|
||||
// objects
|
||||
ErrObjectNotFound = errors.New("object not found")
|
||||
ErrFailedRetrieveObject = errors.New("failed to retrieve object")
|
||||
ErrFailedRetrieveObjects = errors.New("failed to retrieve list of objects")
|
||||
ErrFailedDeleteObject = errors.New("failed to delete object")
|
||||
ErrFailedCreateObject = errors.New("failed to create object")
|
||||
ErrInputMissingSource = errors.New("source is missing for bookmark")
|
||||
ErrFailedSetRelationFeatured = errors.New("failed to set relation featured")
|
||||
ErrFailedFetchBookmark = errors.New("failed to fetch bookmark")
|
||||
ErrFailedPasteBody = errors.New("failed to paste body")
|
||||
|
||||
// types
|
||||
ErrFailedRetrieveTypes = errors.New("failed to retrieve types")
|
||||
ErrTypeNotFound = errors.New("type not found")
|
||||
ErrFailedRetrieveType = errors.New("failed to retrieve type")
|
||||
ErrFailedRetrieveTemplateType = errors.New("failed to retrieve template type")
|
||||
ErrTemplateTypeNotFound = errors.New("template type not found")
|
||||
ErrFailedRetrieveTemplate = errors.New("failed to retrieve template")
|
||||
ErrFailedRetrieveTemplates = errors.New("failed to retrieve templates")
|
||||
ErrTemplateNotFound = errors.New("template not found")
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
ListObjects(ctx context.Context, spaceId string, offset int, limit int) ([]Object, int, bool, error)
|
||||
GetObject(ctx context.Context, spaceId string, objectId string) (Object, error)
|
||||
DeleteObject(ctx context.Context, spaceId string, objectId string) (Object, error)
|
||||
CreateObject(ctx context.Context, spaceId string, request CreateObjectRequest) (Object, error)
|
||||
ListTypes(ctx context.Context, spaceId string, offset int, limit int) ([]Type, int, bool, error)
|
||||
GetType(ctx context.Context, spaceId string, typeId string) (Type, error)
|
||||
ListTemplates(ctx context.Context, spaceId string, typeId string, offset int, limit int) ([]Template, int, bool, error)
|
||||
GetTemplate(ctx context.Context, spaceId string, typeId string, templateId string) (Template, error)
|
||||
}
|
||||
|
||||
type ObjectService struct {
|
||||
mw service.ClientCommandsServer
|
||||
spaceService *space.SpaceService
|
||||
AccountInfo *model.AccountInfo
|
||||
}
|
||||
|
||||
func NewService(mw service.ClientCommandsServer, spaceService *space.SpaceService) *ObjectService {
|
||||
return &ObjectService{mw: mw, spaceService: spaceService}
|
||||
}
|
||||
|
||||
// ListObjects retrieves a paginated list of objects in a specific space.
|
||||
func (s *ObjectService) ListObjects(ctx context.Context, spaceId string, offset int, limit int) (objects []Object, total int, hasMore bool, err error) {
|
||||
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_In,
|
||||
Value: pbtypes.IntList([]int{
|
||||
int(model.ObjectType_basic),
|
||||
int(model.ObjectType_profile),
|
||||
int(model.ObjectType_todo),
|
||||
int(model.ObjectType_note),
|
||||
int(model.ObjectType_bookmark),
|
||||
int(model.ObjectType_set),
|
||||
int(model.ObjectType_collection),
|
||||
int(model.ObjectType_participant),
|
||||
}...),
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{{
|
||||
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
|
||||
Type: model.BlockContentDataviewSort_Desc,
|
||||
Format: model.RelationFormat_longtext,
|
||||
IncludeTime: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
|
||||
}},
|
||||
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String()},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return nil, 0, false, ErrFailedRetrieveObjects
|
||||
}
|
||||
|
||||
total = len(resp.Records)
|
||||
paginatedObjects, hasMore := pagination.Paginate(resp.Records, offset, limit)
|
||||
objects = make([]Object, 0, len(paginatedObjects))
|
||||
|
||||
for _, record := range paginatedObjects {
|
||||
object, err := s.GetObject(ctx, spaceId, record.Fields[bundle.RelationKeyId.String()].GetStringValue())
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
|
||||
objects = append(objects, object)
|
||||
}
|
||||
return objects, total, hasMore, nil
|
||||
}
|
||||
|
||||
// GetObject retrieves a single object by its ID in a specific space.
|
||||
func (s *ObjectService) GetObject(ctx context.Context, spaceId string, objectId string) (Object, error) {
|
||||
resp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
|
||||
SpaceId: spaceId,
|
||||
ObjectId: objectId,
|
||||
})
|
||||
|
||||
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
|
||||
return Object{}, ErrObjectNotFound
|
||||
}
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
|
||||
return Object{}, ErrFailedRetrieveObject
|
||||
}
|
||||
|
||||
icon := util.GetIconFromEmojiOrImage(s.AccountInfo, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconImage.String()].GetStringValue())
|
||||
objectTypeName, err := util.ResolveTypeToName(s.mw, spaceId, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyType.String()].GetStringValue())
|
||||
if err != nil {
|
||||
return Object{}, err
|
||||
}
|
||||
|
||||
object := Object{
|
||||
Type: objectTypeName,
|
||||
Id: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyId.String()].GetStringValue(),
|
||||
Name: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
|
||||
Icon: icon,
|
||||
Snippet: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySnippet.String()].GetStringValue(),
|
||||
Layout: model.ObjectTypeLayout_name[int32(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyLayout.String()].GetNumberValue())],
|
||||
SpaceId: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(),
|
||||
RootId: resp.ObjectView.RootId,
|
||||
Blocks: s.GetBlocks(resp),
|
||||
Details: s.GetDetails(resp),
|
||||
}
|
||||
|
||||
return object, nil
|
||||
}
|
||||
|
||||
// DeleteObject deletes an existing object in a specific space.
|
||||
func (s *ObjectService) DeleteObject(ctx context.Context, spaceId string, objectId string) (Object, error) {
|
||||
object, err := s.GetObject(ctx, spaceId, objectId)
|
||||
if err != nil {
|
||||
return Object{}, err
|
||||
}
|
||||
|
||||
resp := s.mw.ObjectSetIsArchived(ctx, &pb.RpcObjectSetIsArchivedRequest{
|
||||
ContextId: objectId,
|
||||
IsArchived: true,
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSetIsArchivedResponseError_NULL {
|
||||
return Object{}, ErrFailedDeleteObject
|
||||
}
|
||||
|
||||
return object, nil
|
||||
}
|
||||
|
||||
// CreateObject creates a new object in a specific space.
|
||||
func (s *ObjectService) CreateObject(ctx context.Context, spaceId string, request CreateObjectRequest) (Object, error) {
|
||||
if request.ObjectTypeUniqueKey == "ot-bookmark" && request.Source == "" {
|
||||
return Object{}, ErrInputMissingSource
|
||||
}
|
||||
|
||||
details := &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String(request.Name),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String(request.Icon),
|
||||
bundle.RelationKeyDescription.String(): pbtypes.String(request.Description),
|
||||
bundle.RelationKeySource.String(): pbtypes.String(request.Source),
|
||||
bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_api)),
|
||||
},
|
||||
}
|
||||
|
||||
resp := s.mw.ObjectCreate(ctx, &pb.RpcObjectCreateRequest{
|
||||
Details: details,
|
||||
TemplateId: request.TemplateId,
|
||||
SpaceId: spaceId,
|
||||
ObjectTypeUniqueKey: request.ObjectTypeUniqueKey,
|
||||
WithChat: false,
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectCreateResponseError_NULL {
|
||||
return Object{}, ErrFailedCreateObject
|
||||
}
|
||||
|
||||
// ObjectRelationAddFeatured if description was set
|
||||
if request.Description != "" {
|
||||
relAddFeatResp := s.mw.ObjectRelationAddFeatured(ctx, &pb.RpcObjectRelationAddFeaturedRequest{
|
||||
ContextId: resp.ObjectId,
|
||||
Relations: []string{bundle.RelationKeyDescription.String()},
|
||||
})
|
||||
|
||||
if relAddFeatResp.Error.Code != pb.RpcObjectRelationAddFeaturedResponseError_NULL {
|
||||
object, _ := s.GetObject(ctx, spaceId, resp.ObjectId) // nolint:errcheck
|
||||
return object, ErrFailedSetRelationFeatured
|
||||
}
|
||||
}
|
||||
|
||||
// ObjectBookmarkFetch after creating a bookmark object
|
||||
if request.ObjectTypeUniqueKey == "ot-bookmark" {
|
||||
bookmarkResp := s.mw.ObjectBookmarkFetch(ctx, &pb.RpcObjectBookmarkFetchRequest{
|
||||
ContextId: resp.ObjectId,
|
||||
Url: request.Source,
|
||||
})
|
||||
|
||||
if bookmarkResp.Error.Code != pb.RpcObjectBookmarkFetchResponseError_NULL {
|
||||
object, _ := s.GetObject(ctx, spaceId, resp.ObjectId) // nolint:errcheck
|
||||
return object, ErrFailedFetchBookmark
|
||||
}
|
||||
}
|
||||
|
||||
// First call BlockCreate at top, then BlockPaste to paste the body
|
||||
if request.Body != "" {
|
||||
blockCreateResp := s.mw.BlockCreate(ctx, &pb.RpcBlockCreateRequest{
|
||||
ContextId: resp.ObjectId,
|
||||
TargetId: "",
|
||||
Block: &model.Block{
|
||||
Id: "",
|
||||
BackgroundColor: "",
|
||||
Align: model.Block_AlignLeft,
|
||||
VerticalAlign: model.Block_VerticalAlignTop,
|
||||
Content: &model.BlockContentOfText{
|
||||
Text: &model.BlockContentText{
|
||||
Text: "",
|
||||
Style: model.BlockContentText_Paragraph,
|
||||
Checked: false,
|
||||
Color: "",
|
||||
IconEmoji: "",
|
||||
IconImage: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Position: model.Block_Bottom,
|
||||
})
|
||||
|
||||
if blockCreateResp.Error.Code != pb.RpcBlockCreateResponseError_NULL {
|
||||
object, _ := s.GetObject(ctx, spaceId, resp.ObjectId) // nolint:errcheck
|
||||
return object, ErrFailedCreateObject
|
||||
}
|
||||
|
||||
blockPasteResp := s.mw.BlockPaste(ctx, &pb.RpcBlockPasteRequest{
|
||||
ContextId: resp.ObjectId,
|
||||
FocusedBlockId: blockCreateResp.BlockId,
|
||||
TextSlot: request.Body,
|
||||
})
|
||||
|
||||
if blockPasteResp.Error.Code != pb.RpcBlockPasteResponseError_NULL {
|
||||
object, _ := s.GetObject(ctx, spaceId, resp.ObjectId) // nolint:errcheck
|
||||
return object, ErrFailedPasteBody
|
||||
}
|
||||
}
|
||||
|
||||
return s.GetObject(ctx, spaceId, resp.ObjectId)
|
||||
}
|
||||
|
||||
// ListTypes returns a paginated list of types in a specific space.
|
||||
func (s *ObjectService) ListTypes(ctx context.Context, spaceId string, offset int, limit int) (types []Type, total int, hasMore bool, err error) {
|
||||
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.ObjectType_objectType)),
|
||||
},
|
||||
{
|
||||
RelationKey: bundle.RelationKeyIsHidden.String(),
|
||||
Condition: model.BlockContentDataviewFilter_NotEqual,
|
||||
Value: pbtypes.Bool(true),
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyName.String(),
|
||||
Type: model.BlockContentDataviewSort_Asc,
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyUniqueKey.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyRecommendedLayout.String()},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return nil, 0, false, ErrFailedRetrieveTypes
|
||||
}
|
||||
|
||||
total = len(resp.Records)
|
||||
paginatedTypes, hasMore := pagination.Paginate(resp.Records, offset, limit)
|
||||
types = make([]Type, 0, len(paginatedTypes))
|
||||
|
||||
for _, record := range paginatedTypes {
|
||||
types = append(types, Type{
|
||||
Type: "type",
|
||||
Id: record.Fields[bundle.RelationKeyId.String()].GetStringValue(),
|
||||
UniqueKey: record.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue(),
|
||||
Name: record.Fields[bundle.RelationKeyName.String()].GetStringValue(),
|
||||
Icon: record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(),
|
||||
RecommendedLayout: model.ObjectTypeLayout_name[int32(record.Fields[bundle.RelationKeyRecommendedLayout.String()].GetNumberValue())],
|
||||
})
|
||||
}
|
||||
return types, total, hasMore, nil
|
||||
}
|
||||
|
||||
// GetType returns a single type by its ID in a specific space.
|
||||
func (s *ObjectService) GetType(ctx context.Context, spaceId string, typeId string) (Type, error) {
|
||||
resp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
|
||||
SpaceId: spaceId,
|
||||
ObjectId: typeId,
|
||||
})
|
||||
|
||||
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
|
||||
return Type{}, ErrTypeNotFound
|
||||
}
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
|
||||
return Type{}, ErrFailedRetrieveType
|
||||
}
|
||||
|
||||
return Type{
|
||||
Type: "type",
|
||||
Id: typeId,
|
||||
UniqueKey: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyUniqueKey.String()].GetStringValue(),
|
||||
Name: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
|
||||
Icon: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(),
|
||||
RecommendedLayout: model.ObjectTypeLayout_name[int32(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyRecommendedLayout.String()].GetNumberValue())],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListTemplates returns a paginated list of templates in a specific space.
|
||||
func (s *ObjectService) ListTemplates(ctx context.Context, spaceId string, typeId string, offset int, limit int) (templates []Template, total int, hasMore bool, err error) {
|
||||
// First, determine the type ID of "ot-template" in the space
|
||||
templateTypeIdResp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyUniqueKey.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String("ot-template"),
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyId.String()},
|
||||
})
|
||||
|
||||
if templateTypeIdResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return nil, 0, false, ErrFailedRetrieveTemplateType
|
||||
}
|
||||
|
||||
if len(templateTypeIdResp.Records) == 0 {
|
||||
return nil, 0, false, ErrTemplateTypeNotFound
|
||||
}
|
||||
|
||||
// Then, search all objects of the template type and filter by the target object type
|
||||
templateTypeId := templateTypeIdResp.Records[0].Fields[bundle.RelationKeyId.String()].GetStringValue()
|
||||
templateObjectsResp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyType.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(templateTypeId),
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyTargetObjectType.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String()},
|
||||
})
|
||||
|
||||
if templateObjectsResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return nil, 0, false, ErrFailedRetrieveTemplates
|
||||
}
|
||||
|
||||
templateIds := make([]string, 0)
|
||||
for _, record := range templateObjectsResp.Records {
|
||||
if record.Fields[bundle.RelationKeyTargetObjectType.String()].GetStringValue() == typeId {
|
||||
templateIds = append(templateIds, record.Fields[bundle.RelationKeyId.String()].GetStringValue())
|
||||
}
|
||||
}
|
||||
|
||||
total = len(templateIds)
|
||||
paginatedTemplates, hasMore := pagination.Paginate(templateIds, offset, limit)
|
||||
templates = make([]Template, 0, len(paginatedTemplates))
|
||||
|
||||
// Finally, open each template and populate the response
|
||||
for _, templateId := range paginatedTemplates {
|
||||
templateResp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
|
||||
SpaceId: spaceId,
|
||||
ObjectId: templateId,
|
||||
})
|
||||
|
||||
if templateResp.Error.Code != pb.RpcObjectShowResponseError_NULL {
|
||||
return nil, 0, false, ErrFailedRetrieveTemplate
|
||||
}
|
||||
|
||||
templates = append(templates, Template{
|
||||
Type: "template",
|
||||
Id: templateId,
|
||||
Name: templateResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
|
||||
Icon: templateResp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(),
|
||||
})
|
||||
}
|
||||
|
||||
return templates, total, hasMore, nil
|
||||
}
|
||||
|
||||
// GetTemplate returns a single template by its ID in a specific space.
|
||||
func (s *ObjectService) GetTemplate(ctx context.Context, spaceId string, typeId string, templateId string) (Template, error) {
|
||||
resp := s.mw.ObjectShow(ctx, &pb.RpcObjectShowRequest{
|
||||
SpaceId: spaceId,
|
||||
ObjectId: templateId,
|
||||
})
|
||||
|
||||
if resp.Error.Code == pb.RpcObjectShowResponseError_NOT_FOUND {
|
||||
return Template{}, ErrTemplateNotFound
|
||||
}
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectShowResponseError_NULL {
|
||||
return Template{}, ErrFailedRetrieveTemplate
|
||||
}
|
||||
|
||||
return Template{
|
||||
Type: "template",
|
||||
Id: templateId,
|
||||
Name: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
|
||||
Icon: resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetDetails returns the list of details from the ObjectShowResponse.
|
||||
func (s *ObjectService) GetDetails(resp *pb.RpcObjectShowResponse) []Detail {
|
||||
creator := resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyCreator.String()].GetStringValue()
|
||||
lastModifiedBy := resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyLastModifiedBy.String()].GetStringValue()
|
||||
|
||||
var creatorId, lastModifiedById string
|
||||
for _, detail := range resp.ObjectView.Details {
|
||||
if detail.Id == creator {
|
||||
creatorId = detail.Id
|
||||
}
|
||||
if detail.Id == lastModifiedBy {
|
||||
lastModifiedById = detail.Id
|
||||
}
|
||||
}
|
||||
|
||||
return []Detail{
|
||||
{
|
||||
Id: "last_modified_date",
|
||||
Details: map[string]interface{}{
|
||||
"last_modified_date": PosixToISO8601(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyLastModifiedDate.String()].GetNumberValue()),
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "last_modified_by",
|
||||
Details: map[string]interface{}{
|
||||
"details": s.spaceService.GetParticipantDetails(s.mw, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(), lastModifiedById),
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "created_date",
|
||||
Details: map[string]interface{}{
|
||||
"created_date": PosixToISO8601(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyCreatedDate.String()].GetNumberValue()),
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "created_by",
|
||||
Details: map[string]interface{}{
|
||||
"details": s.spaceService.GetParticipantDetails(s.mw, resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(), creatorId),
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "last_opened_date",
|
||||
Details: map[string]interface{}{
|
||||
"last_opened_date": PosixToISO8601(resp.ObjectView.Details[0].Details.Fields[bundle.RelationKeyLastOpenedDate.String()].GetNumberValue()),
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "tags",
|
||||
Details: map[string]interface{}{
|
||||
"tags": s.getTags(resp),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// getTags returns the list of tags from the ObjectShowResponse
|
||||
func (s *ObjectService) getTags(resp *pb.RpcObjectShowResponse) []Tag {
|
||||
tags := []Tag{}
|
||||
|
||||
tagField, ok := resp.ObjectView.Details[0].Details.Fields["tag"]
|
||||
if !ok || tagField.GetListValue() == nil {
|
||||
return tags
|
||||
}
|
||||
|
||||
for _, tagId := range tagField.GetListValue().Values {
|
||||
id := tagId.GetStringValue()
|
||||
for _, detail := range resp.ObjectView.Details {
|
||||
if detail.Id == id {
|
||||
tags = append(tags, Tag{
|
||||
Id: id,
|
||||
Name: detail.Details.Fields[bundle.RelationKeyName.String()].GetStringValue(),
|
||||
Color: detail.Details.Fields[bundle.RelationKeyRelationOptionColor.String()].GetStringValue(),
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
// GetBlocks returns the list of blocks from the ObjectShowResponse.
|
||||
func (s *ObjectService) GetBlocks(resp *pb.RpcObjectShowResponse) []Block {
|
||||
blocks := []Block{}
|
||||
|
||||
for _, block := range resp.ObjectView.Blocks {
|
||||
var text *Text
|
||||
var file *File
|
||||
|
||||
switch content := block.Content.(type) {
|
||||
case *model.BlockContentOfText:
|
||||
text = &Text{
|
||||
Text: content.Text.Text,
|
||||
Style: model.BlockContentTextStyle_name[int32(content.Text.Style)],
|
||||
Checked: content.Text.Checked,
|
||||
Color: content.Text.Color,
|
||||
Icon: util.GetIconFromEmojiOrImage(s.AccountInfo, content.Text.IconEmoji, content.Text.IconImage),
|
||||
}
|
||||
case *model.BlockContentOfFile:
|
||||
file = &File{
|
||||
Hash: content.File.Hash,
|
||||
Name: content.File.Name,
|
||||
Type: model.BlockContentFileType_name[int32(content.File.Type)],
|
||||
Mime: content.File.Mime,
|
||||
Size: content.File.Size(),
|
||||
AddedAt: int(content.File.AddedAt),
|
||||
TargetObjectId: content.File.TargetObjectId,
|
||||
State: model.BlockContentFileState_name[int32(content.File.State)],
|
||||
Style: model.BlockContentFileStyle_name[int32(content.File.Style)],
|
||||
}
|
||||
// TODO: other content types?
|
||||
}
|
||||
|
||||
blocks = append(blocks, Block{
|
||||
Id: block.Id,
|
||||
ChildrenIds: block.ChildrenIds,
|
||||
BackgroundColor: block.BackgroundColor,
|
||||
Align: model.BlockAlign_name[int32(block.Align)],
|
||||
VerticalAlign: model.BlockVerticalAlign_name[int32(block.VerticalAlign)],
|
||||
Text: text,
|
||||
File: file,
|
||||
})
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
func PosixToISO8601(posix float64) string {
|
||||
t := time.Unix(int64(posix), 0).UTC()
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
781
core/api/internal/object/service_test.go
Normal file
781
core/api/internal/object/service_test.go
Normal file
|
@ -0,0 +1,781 @@
|
|||
package object
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/space"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service/mock_service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
const (
|
||||
offset = 0
|
||||
limit = 100
|
||||
mockedTechSpaceId = "mocked-tech-space-id"
|
||||
gatewayUrl = "http://localhost:31006"
|
||||
mockedSpaceId = "mocked-space-id"
|
||||
mockedObjectId = "mocked-object-id"
|
||||
mockedObjectType = "mocked-object-type"
|
||||
mockedNewObjectId = "mocked-new-object-id"
|
||||
mockedObjectName = "mocked-object-name"
|
||||
mockedObjectSnippet = "mocked-object-snippet"
|
||||
mockedObjectIcon = "🔍"
|
||||
mockedObjectTypeUniqueKey = "ot-page"
|
||||
mockedTypeId = "mocked-type-id"
|
||||
mockedTypeName = "mocked-type-name"
|
||||
mockedTypeUniqueKey = "mocked-type-unique-key"
|
||||
mockedTypeIcon = "📝"
|
||||
mockedTemplateId = "mocked-template-id"
|
||||
mockedTemplateName = "mocked-template-name"
|
||||
mockedTemplateIcon = "📃"
|
||||
)
|
||||
|
||||
type fixture struct {
|
||||
*ObjectService
|
||||
mwMock *mock_service.MockClientCommandsServer
|
||||
}
|
||||
|
||||
func newFixture(t *testing.T) *fixture {
|
||||
mw := mock_service.NewMockClientCommandsServer(t)
|
||||
|
||||
spaceService := space.NewService(mw)
|
||||
objectService := NewService(mw, spaceService)
|
||||
objectService.AccountInfo = &model.AccountInfo{
|
||||
TechSpaceId: mockedTechSpaceId,
|
||||
GatewayUrl: gatewayUrl,
|
||||
}
|
||||
|
||||
return &fixture{
|
||||
ObjectService: objectService,
|
||||
mwMock: mw,
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectService_ListObjects(t *testing.T) {
|
||||
t.Run("successfully get objects for a space", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_In,
|
||||
Value: pbtypes.IntList([]int{
|
||||
int(model.ObjectType_basic),
|
||||
int(model.ObjectType_profile),
|
||||
int(model.ObjectType_todo),
|
||||
int(model.ObjectType_note),
|
||||
int(model.ObjectType_bookmark),
|
||||
int(model.ObjectType_set),
|
||||
int(model.ObjectType_collection),
|
||||
int(model.ObjectType_participant),
|
||||
}...),
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{{
|
||||
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
|
||||
Type: model.BlockContentDataviewSort_Desc,
|
||||
Format: model.RelationFormat_longtext,
|
||||
IncludeTime: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
|
||||
}},
|
||||
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String()},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
|
||||
bundle.RelationKeySnippet.String(): pbtypes.String(mockedObjectSnippet),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
|
||||
bundle.RelationKeyType.String(): pbtypes.String(mockedObjectTypeUniqueKey),
|
||||
bundle.RelationKeyLayout.String(): pbtypes.Float64(float64(model.ObjectType_basic)),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock object show for object details
|
||||
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
ObjectId: mockedObjectId,
|
||||
}).Return(&pb.RpcObjectShowResponse{
|
||||
ObjectView: &model.ObjectView{
|
||||
RootId: mockedObjectId,
|
||||
Details: []*model.ObjectViewDetailsSet{
|
||||
{
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
|
||||
bundle.RelationKeySnippet.String(): pbtypes.String(mockedObjectSnippet),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
|
||||
bundle.RelationKeyType.String(): pbtypes.String(mockedObjectTypeUniqueKey),
|
||||
bundle.RelationKeyCreatedDate.String(): pbtypes.Float64(888888),
|
||||
bundle.RelationKeyLastModifiedDate.String(): pbtypes.Float64(999999),
|
||||
bundle.RelationKeyLastOpenedDate.String(): pbtypes.Float64(0),
|
||||
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock type resolution
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyUniqueKey.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(mockedObjectTypeUniqueKey),
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyName.String()},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectType),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock participant details
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyId.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(""),
|
||||
},
|
||||
},
|
||||
Keys: []string{
|
||||
bundle.RelationKeyId.String(),
|
||||
bundle.RelationKeyName.String(),
|
||||
bundle.RelationKeyIconEmoji.String(),
|
||||
bundle.RelationKeyIconImage.String(),
|
||||
bundle.RelationKeyIdentity.String(),
|
||||
bundle.RelationKeyGlobalName.String(),
|
||||
bundle.RelationKeyParticipantPermissions.String(),
|
||||
},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
Records: []*types.Struct{
|
||||
{},
|
||||
},
|
||||
}).Twice()
|
||||
|
||||
// when
|
||||
objects, total, hasMore, err := fx.ListObjects(ctx, mockedSpaceId, offset, limit)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Len(t, objects, 1)
|
||||
require.Equal(t, mockedObjectType, objects[0].Type)
|
||||
require.Equal(t, mockedObjectId, objects[0].Id)
|
||||
require.Equal(t, mockedObjectName, objects[0].Name)
|
||||
require.Equal(t, mockedObjectSnippet, objects[0].Snippet)
|
||||
require.Equal(t, mockedObjectIcon, objects[0].Icon)
|
||||
require.Equal(t, 6, len(objects[0].Details))
|
||||
|
||||
for _, detail := range objects[0].Details {
|
||||
if detail.Id == "created_date" {
|
||||
require.Equal(t, "1970-01-11T06:54:48Z", detail.Details["created_date"])
|
||||
} else if detail.Id == "created_by" {
|
||||
require.Empty(t, detail.Details["created_by"])
|
||||
} else if detail.Id == "last_modified_date" {
|
||||
require.Equal(t, "1970-01-12T13:46:39Z", detail.Details["last_modified_date"])
|
||||
} else if detail.Id == "last_modified_by" {
|
||||
require.Empty(t, detail.Details["last_modified_by"])
|
||||
} else if detail.Id == "last_opened_date" {
|
||||
require.Equal(t, "1970-01-01T00:00:00Z", detail.Details["last_opened_date"])
|
||||
} else if detail.Id == "tags" {
|
||||
require.Empty(t, detail.Details["tags"])
|
||||
} else {
|
||||
t.Errorf("unexpected detail id: %s", detail.Id)
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, 1, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
|
||||
t.Run("no objects found", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
objects, total, hasMore, err := fx.ListObjects(ctx, "empty-space", offset, limit)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Len(t, objects, 0)
|
||||
require.Equal(t, 0, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
}
|
||||
|
||||
func TestObjectService_GetObject(t *testing.T) {
|
||||
t.Run("object found", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
ObjectId: mockedObjectId,
|
||||
}).
|
||||
Return(&pb.RpcObjectShowResponse{
|
||||
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
|
||||
ObjectView: &model.ObjectView{
|
||||
RootId: mockedObjectId,
|
||||
Details: []*model.ObjectViewDetailsSet{
|
||||
{
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
|
||||
bundle.RelationKeySnippet.String(): pbtypes.String(mockedObjectSnippet),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectName),
|
||||
bundle.RelationKeyType.String(): pbtypes.String(mockedObjectTypeUniqueKey),
|
||||
bundle.RelationKeyLastModifiedDate.String(): pbtypes.Float64(999999),
|
||||
bundle.RelationKeyCreatedDate.String(): pbtypes.Float64(888888),
|
||||
bundle.RelationKeyLastOpenedDate.String(): pbtypes.Float64(0),
|
||||
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
// Mock type resolution
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyUniqueKey.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(mockedObjectTypeUniqueKey),
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyName.String()},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectType),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
// Mock participant details
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyId.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(""),
|
||||
},
|
||||
},
|
||||
Keys: []string{
|
||||
bundle.RelationKeyId.String(),
|
||||
bundle.RelationKeyName.String(),
|
||||
bundle.RelationKeyIconEmoji.String(),
|
||||
bundle.RelationKeyIconImage.String(),
|
||||
bundle.RelationKeyIdentity.String(),
|
||||
bundle.RelationKeyGlobalName.String(),
|
||||
bundle.RelationKeyParticipantPermissions.String(),
|
||||
},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
Records: []*types.Struct{
|
||||
{},
|
||||
},
|
||||
}).Twice()
|
||||
|
||||
// when
|
||||
object, err := fx.GetObject(ctx, mockedSpaceId, mockedObjectId)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockedObjectType, object.Type)
|
||||
require.Equal(t, mockedObjectId, object.Id)
|
||||
require.Equal(t, mockedObjectName, object.Name)
|
||||
require.Equal(t, mockedObjectSnippet, object.Snippet)
|
||||
require.Equal(t, mockedObjectName, object.Icon)
|
||||
require.Equal(t, 6, len(object.Details))
|
||||
|
||||
for _, detail := range object.Details {
|
||||
if detail.Id == "created_date" {
|
||||
require.Equal(t, "1970-01-11T06:54:48Z", detail.Details["created_date"])
|
||||
} else if detail.Id == "created_by" {
|
||||
require.Empty(t, detail.Details["created_by"])
|
||||
} else if detail.Id == "last_modified_date" {
|
||||
require.Equal(t, "1970-01-12T13:46:39Z", detail.Details["last_modified_date"])
|
||||
} else if detail.Id == "last_modified_by" {
|
||||
require.Empty(t, detail.Details["last_modified_by"])
|
||||
} else if detail.Id == "last_opened_date" {
|
||||
require.Equal(t, "1970-01-01T00:00:00Z", detail.Details["last_opened_date"])
|
||||
} else if detail.Id == "tags" {
|
||||
require.Empty(t, detail.Details["tags"])
|
||||
} else {
|
||||
t.Errorf("unexpected detail id: %s", detail.Id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("object not found", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectShow", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcObjectShowResponse{
|
||||
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NOT_FOUND},
|
||||
}, nil).Once()
|
||||
|
||||
// when
|
||||
object, err := fx.GetObject(ctx, mockedSpaceId, "missing-obj")
|
||||
|
||||
// then
|
||||
require.ErrorIs(t, err, ErrObjectNotFound)
|
||||
require.Empty(t, object)
|
||||
})
|
||||
}
|
||||
|
||||
func TestObjectService_CreateObject(t *testing.T) {
|
||||
t.Run("successful object creation", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectCreate", mock.Anything, &pb.RpcObjectCreateRequest{
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
|
||||
bundle.RelationKeyDescription.String(): pbtypes.String(""),
|
||||
bundle.RelationKeySource.String(): pbtypes.String(""),
|
||||
bundle.RelationKeyOrigin.String(): pbtypes.Int64(int64(model.ObjectOrigin_api)),
|
||||
},
|
||||
},
|
||||
TemplateId: "",
|
||||
SpaceId: mockedSpaceId,
|
||||
ObjectTypeUniqueKey: mockedObjectTypeUniqueKey,
|
||||
WithChat: false,
|
||||
}).Return(&pb.RpcObjectCreateResponse{
|
||||
ObjectId: mockedNewObjectId,
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedNewObjectId),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
|
||||
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectCreateResponseError{Code: pb.RpcObjectCreateResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock object show for object details
|
||||
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
ObjectId: mockedNewObjectId,
|
||||
}).Return(&pb.RpcObjectShowResponse{
|
||||
ObjectView: &model.ObjectView{
|
||||
RootId: mockedNewObjectId,
|
||||
Details: []*model.ObjectViewDetailsSet{
|
||||
{
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedNewObjectId),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
|
||||
bundle.RelationKeyLayout.String(): pbtypes.Float64(float64(model.ObjectType_basic)),
|
||||
bundle.RelationKeyType.String(): pbtypes.String(mockedObjectTypeUniqueKey),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedObjectIcon),
|
||||
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock type resolution
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyUniqueKey.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(mockedObjectTypeUniqueKey),
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyName.String()},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectType),
|
||||
},
|
||||
},
|
||||
},
|
||||
}).Once()
|
||||
|
||||
// Mock participant details
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyId.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(""),
|
||||
},
|
||||
},
|
||||
Keys: []string{
|
||||
bundle.RelationKeyId.String(),
|
||||
bundle.RelationKeyName.String(),
|
||||
bundle.RelationKeyIconEmoji.String(),
|
||||
bundle.RelationKeyIconImage.String(),
|
||||
bundle.RelationKeyIdentity.String(),
|
||||
bundle.RelationKeyGlobalName.String(),
|
||||
bundle.RelationKeyParticipantPermissions.String(),
|
||||
},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
Records: []*types.Struct{
|
||||
{},
|
||||
},
|
||||
}).Twice()
|
||||
|
||||
// when
|
||||
object, err := fx.CreateObject(ctx, mockedSpaceId, CreateObjectRequest{
|
||||
Name: mockedObjectName,
|
||||
Icon: mockedObjectIcon,
|
||||
// TODO: use actual values
|
||||
TemplateId: "",
|
||||
ObjectTypeUniqueKey: mockedObjectTypeUniqueKey,
|
||||
})
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockedObjectType, object.Type)
|
||||
require.Equal(t, mockedNewObjectId, object.Id)
|
||||
require.Equal(t, mockedObjectName, object.Name)
|
||||
require.Equal(t, mockedObjectIcon, object.Icon)
|
||||
require.Equal(t, mockedSpaceId, object.SpaceId)
|
||||
})
|
||||
|
||||
t.Run("creation error", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectCreate", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcObjectCreateResponse{
|
||||
Error: &pb.RpcObjectCreateResponseError{Code: pb.RpcObjectCreateResponseError_UNKNOWN_ERROR},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
object, err := fx.CreateObject(ctx, mockedSpaceId, CreateObjectRequest{
|
||||
Name: "Fail Object",
|
||||
Icon: "",
|
||||
})
|
||||
|
||||
// then
|
||||
require.ErrorIs(t, err, ErrFailedCreateObject)
|
||||
require.Empty(t, object)
|
||||
})
|
||||
}
|
||||
|
||||
func TestObjectService_ListTypes(t *testing.T) {
|
||||
t.Run("types found", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String("type-1"),
|
||||
bundle.RelationKeyName.String(): pbtypes.String("Type One"),
|
||||
bundle.RelationKeyUniqueKey.String(): pbtypes.String("type-one-key"),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String("🗂️"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
types, total, hasMore, err := fx.ListTypes(ctx, mockedSpaceId, offset, limit)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Len(t, types, 1)
|
||||
require.Equal(t, "type-1", types[0].Id)
|
||||
require.Equal(t, "Type One", types[0].Name)
|
||||
require.Equal(t, "type-one-key", types[0].UniqueKey)
|
||||
require.Equal(t, "🗂️", types[0].Icon)
|
||||
require.Equal(t, 1, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
|
||||
t.Run("no types found", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
types, total, hasMore, err := fx.ListTypes(ctx, "empty-space", offset, limit)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Len(t, types, 0)
|
||||
require.Equal(t, 0, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
}
|
||||
|
||||
func TestObjectService_GetType(t *testing.T) {
|
||||
t.Run("type found", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
ObjectId: mockedTypeId,
|
||||
}).Return(&pb.RpcObjectShowResponse{
|
||||
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
|
||||
ObjectView: &model.ObjectView{
|
||||
Details: []*model.ObjectViewDetailsSet{
|
||||
{
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedTypeId),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedTypeName),
|
||||
bundle.RelationKeyUniqueKey.String(): pbtypes.String(mockedTypeUniqueKey),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedTypeIcon),
|
||||
bundle.RelationKeyRecommendedLayout.String(): pbtypes.Float64(float64(model.ObjectType_basic)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
objType, err := fx.GetType(ctx, mockedSpaceId, mockedTypeId)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockedTypeId, objType.Id)
|
||||
require.Equal(t, mockedTypeName, objType.Name)
|
||||
require.Equal(t, mockedTypeUniqueKey, objType.UniqueKey)
|
||||
require.Equal(t, mockedTypeIcon, objType.Icon)
|
||||
require.Equal(t, model.ObjectTypeLayout_name[int32(model.ObjectType_basic)], objType.RecommendedLayout)
|
||||
})
|
||||
|
||||
t.Run("type not found", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
ObjectId: mockedTypeId,
|
||||
}).Return(&pb.RpcObjectShowResponse{
|
||||
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NOT_FOUND},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
objType, err := fx.GetType(ctx, mockedSpaceId, mockedTypeId)
|
||||
|
||||
// then
|
||||
require.ErrorIs(t, err, ErrTypeNotFound)
|
||||
require.Empty(t, objType)
|
||||
})
|
||||
}
|
||||
|
||||
func TestObjectService_ListTemplates(t *testing.T) {
|
||||
t.Run("templates found", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
// Mock template type search
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String("template-type-id"),
|
||||
bundle.RelationKeyUniqueKey.String(): pbtypes.String("ot-template"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock actual template objects search
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String("template-1"),
|
||||
bundle.RelationKeyTargetObjectType.String(): pbtypes.String("target-type-id"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock object show for template details
|
||||
fx.mwMock.On("ObjectShow", mock.Anything, mock.Anything).Return(&pb.RpcObjectShowResponse{
|
||||
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
|
||||
ObjectView: &model.ObjectView{
|
||||
Details: []*model.ObjectViewDetailsSet{
|
||||
{
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String("Template Name"),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String("📝"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
// when
|
||||
templates, total, hasMore, err := fx.ListTemplates(ctx, mockedSpaceId, "target-type-id", offset, limit)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Len(t, templates, 1)
|
||||
require.Equal(t, "template-1", templates[0].Id)
|
||||
require.Equal(t, "Template Name", templates[0].Name)
|
||||
require.Equal(t, "📝", templates[0].Icon)
|
||||
require.Equal(t, 1, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
|
||||
t.Run("no template type found", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
templates, total, hasMore, err := fx.ListTemplates(ctx, mockedSpaceId, "missing-type-id", offset, limit)
|
||||
|
||||
// then
|
||||
require.ErrorIs(t, err, ErrTemplateTypeNotFound)
|
||||
require.Len(t, templates, 0)
|
||||
require.Equal(t, 0, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
}
|
||||
|
||||
func TestObjectService_GetTemplate(t *testing.T) {
|
||||
t.Run("template found", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
ObjectId: mockedTemplateId,
|
||||
}).Return(&pb.RpcObjectShowResponse{
|
||||
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
|
||||
ObjectView: &model.ObjectView{
|
||||
Details: []*model.ObjectViewDetailsSet{
|
||||
{
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedTemplateId),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedTemplateName),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedTemplateIcon),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
template, err := fx.GetTemplate(ctx, mockedSpaceId, mockedObjectType, mockedTemplateId)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockedTemplateId, template.Id)
|
||||
require.Equal(t, mockedTemplateName, template.Name)
|
||||
require.Equal(t, mockedTemplateIcon, template.Icon)
|
||||
})
|
||||
|
||||
t.Run("template not found", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
ObjectId: mockedTemplateId,
|
||||
}).Return(&pb.RpcObjectShowResponse{
|
||||
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NOT_FOUND},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
template, err := fx.GetTemplate(ctx, mockedSpaceId, mockedTypeId, mockedTemplateId)
|
||||
|
||||
// then
|
||||
require.ErrorIs(t, err, ErrTemplateNotFound)
|
||||
require.Empty(t, template)
|
||||
})
|
||||
}
|
92
core/api/internal/search/handler.go
Normal file
92
core/api/internal/search/handler.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/core/api/util"
|
||||
)
|
||||
|
||||
// GlobalSearchHandler searches and retrieves objects across all spaces
|
||||
//
|
||||
// @Summary Search objects across all spaces
|
||||
// @Tags search
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
|
||||
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
|
||||
// @Param request body SearchRequest true "Search parameters"
|
||||
// @Success 200 {object} pagination.PaginatedResponse[object.Object] "List of objects"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /search [post]
|
||||
func GlobalSearchHandler(s *SearchService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
request := SearchRequest{}
|
||||
if err := c.BindJSON(&request); err != nil {
|
||||
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
|
||||
c.JSON(http.StatusBadRequest, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
objects, total, hasMore, err := s.GlobalSearch(c, request, offset, limit)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrFailedSearchObjects, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, objects, total, offset, limit, hasMore)
|
||||
}
|
||||
}
|
||||
|
||||
// SearchHandler searches and retrieves objects within a space
|
||||
//
|
||||
// @Summary Search objects within a space
|
||||
// @Tags search
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "Space ID"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
|
||||
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
|
||||
// @Param request body SearchRequest true "Search parameters"
|
||||
// @Success 200 {object} pagination.PaginatedResponse[object.Object] "List of objects"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/search [post]
|
||||
func SearchHandler(s *SearchService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceID := c.Param("space_id")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
request := SearchRequest{}
|
||||
if err := c.BindJSON(&request); err != nil {
|
||||
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
|
||||
c.JSON(http.StatusBadRequest, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
objects, total, hasMore, err := s.Search(c, spaceID, request, offset, limit)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrFailedSearchObjects, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, objects, total, offset, limit, hasMore)
|
||||
}
|
||||
}
|
12
core/api/internal/search/model.go
Normal file
12
core/api/internal/search/model.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package search
|
||||
|
||||
type SearchRequest struct {
|
||||
Query string `json:"query"`
|
||||
Types []string `json:"types"`
|
||||
Sort SortOptions `json:"sort"`
|
||||
}
|
||||
|
||||
type SortOptions struct {
|
||||
Direction string `json:"direction" enums:"asc,desc" default:"desc"`
|
||||
Timestamp string `json:"timestamp" enums:"created_date,last_modified_date,last_opened_date" default:"last_modified_date"`
|
||||
}
|
299
core/api/internal/search/service.go
Normal file
299
core/api/internal/search/service.go
Normal file
|
@ -0,0 +1,299 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/object"
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/space"
|
||||
"github.com/anyproto/anytype-heart/core/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/core/api/util"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
var (
|
||||
spaceLimit = 64
|
||||
ErrFailedSearchObjects = errors.New("failed to retrieve objects from space")
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GlobalSearch(ctx context.Context, request SearchRequest, offset int, limit int) (objects []object.Object, total int, hasMore bool, err error)
|
||||
Search(ctx context.Context, spaceId string, request SearchRequest, offset int, limit int) (objects []object.Object, total int, hasMore bool, err error)
|
||||
}
|
||||
|
||||
type SearchService struct {
|
||||
mw service.ClientCommandsServer
|
||||
spaceService *space.SpaceService
|
||||
objectService *object.ObjectService
|
||||
AccountInfo *model.AccountInfo
|
||||
}
|
||||
|
||||
func NewService(mw service.ClientCommandsServer, spaceService *space.SpaceService, objectService *object.ObjectService) *SearchService {
|
||||
return &SearchService{mw: mw, spaceService: spaceService, objectService: objectService}
|
||||
}
|
||||
|
||||
// GlobalSearch retrieves a paginated list of objects from all spaces that match the search parameters.
|
||||
func (s *SearchService) GlobalSearch(ctx context.Context, request SearchRequest, offset int, limit int) (objects []object.Object, total int, hasMore bool, err error) {
|
||||
spaces, _, _, err := s.spaceService.ListSpaces(ctx, 0, spaceLimit)
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
|
||||
baseFilters := s.prepareBaseFilters()
|
||||
queryFilters := s.prepareQueryFilter(request.Query)
|
||||
sorts := s.prepareSorts(request.Sort)
|
||||
dateToSortAfter := sorts.RelationKey
|
||||
|
||||
allResponses := make([]*pb.RpcObjectSearchResponse, 0, len(spaces))
|
||||
for _, space := range spaces {
|
||||
// Resolve object type IDs per space, as they are unique per space
|
||||
typeFilters := s.prepareObjectTypeFilters(space.Id, request.Types)
|
||||
filters := s.combineFilters(model.BlockContentDataviewFilter_And, baseFilters, queryFilters, typeFilters)
|
||||
|
||||
objResp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: space.Id,
|
||||
Filters: filters,
|
||||
Sorts: []*model.BlockContentDataviewSort{sorts},
|
||||
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeySpaceId.String(), dateToSortAfter},
|
||||
Limit: int32(offset + limit), // nolint: gosec
|
||||
})
|
||||
|
||||
if objResp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return nil, 0, false, ErrFailedSearchObjects
|
||||
}
|
||||
|
||||
allResponses = append(allResponses, objResp)
|
||||
}
|
||||
|
||||
combinedRecords := make([]struct {
|
||||
Id string
|
||||
SpaceId string
|
||||
DateToSortAfter float64
|
||||
}, 0)
|
||||
for _, objResp := range allResponses {
|
||||
for _, record := range objResp.Records {
|
||||
combinedRecords = append(combinedRecords, struct {
|
||||
Id string
|
||||
SpaceId string
|
||||
DateToSortAfter float64
|
||||
}{
|
||||
Id: record.Fields[bundle.RelationKeyId.String()].GetStringValue(),
|
||||
SpaceId: record.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(),
|
||||
DateToSortAfter: record.Fields[dateToSortAfter].GetNumberValue(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// sort after posix last_modified_date to achieve descending sort order across all spaces
|
||||
sort.Slice(combinedRecords, func(i, j int) bool {
|
||||
return combinedRecords[i].DateToSortAfter > combinedRecords[j].DateToSortAfter
|
||||
})
|
||||
|
||||
total = len(combinedRecords)
|
||||
paginatedRecords, hasMore := pagination.Paginate(combinedRecords, offset, limit)
|
||||
|
||||
results := make([]object.Object, 0, len(paginatedRecords))
|
||||
for _, record := range paginatedRecords {
|
||||
object, err := s.objectService.GetObject(ctx, record.SpaceId, record.Id)
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
results = append(results, object)
|
||||
}
|
||||
|
||||
return results, total, hasMore, nil
|
||||
}
|
||||
|
||||
// Search retrieves a paginated list of objects from a specific space that match the search parameters.
|
||||
func (s *SearchService) Search(ctx context.Context, spaceId string, request SearchRequest, offset int, limit int) (objects []object.Object, total int, hasMore bool, err error) {
|
||||
baseFilters := s.prepareBaseFilters()
|
||||
queryFilters := s.prepareQueryFilter(request.Query)
|
||||
typeFilters := s.prepareObjectTypeFilters(spaceId, request.Types)
|
||||
filters := s.combineFilters(model.BlockContentDataviewFilter_And, baseFilters, queryFilters, typeFilters)
|
||||
|
||||
sorts := s.prepareSorts(request.Sort)
|
||||
dateToSortAfter := sorts.RelationKey
|
||||
|
||||
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: filters,
|
||||
Sorts: []*model.BlockContentDataviewSort{sorts},
|
||||
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeySpaceId.String(), dateToSortAfter},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return nil, 0, false, ErrFailedSearchObjects
|
||||
}
|
||||
|
||||
total = len(resp.Records)
|
||||
paginatedRecords, hasMore := pagination.Paginate(resp.Records, offset, limit)
|
||||
|
||||
results := make([]object.Object, 0, len(paginatedRecords))
|
||||
for _, record := range paginatedRecords {
|
||||
object, err := s.objectService.GetObject(ctx, record.Fields[bundle.RelationKeySpaceId.String()].GetStringValue(), record.Fields[bundle.RelationKeyId.String()].GetStringValue())
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
results = append(results, object)
|
||||
}
|
||||
|
||||
return results, total, hasMore, nil
|
||||
}
|
||||
|
||||
// makeAndCondition combines multiple filter groups with the given operator.
|
||||
func (s *SearchService) combineFilters(operator model.BlockContentDataviewFilterOperator, filterGroups ...[]*model.BlockContentDataviewFilter) []*model.BlockContentDataviewFilter {
|
||||
nestedFilters := make([]*model.BlockContentDataviewFilter, 0)
|
||||
for _, group := range filterGroups {
|
||||
if len(group) > 0 {
|
||||
nestedFilters = append(nestedFilters, group...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(nestedFilters) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: operator,
|
||||
NestedFilters: nestedFilters,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// prepareBaseFilters returns a list of default filters that should be applied to all search queries.
|
||||
func (s *SearchService) prepareBaseFilters() []*model.BlockContentDataviewFilter {
|
||||
return []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_In,
|
||||
Value: pbtypes.IntList([]int{
|
||||
int(model.ObjectType_basic),
|
||||
int(model.ObjectType_profile),
|
||||
int(model.ObjectType_todo),
|
||||
int(model.ObjectType_note),
|
||||
int(model.ObjectType_bookmark),
|
||||
int(model.ObjectType_set),
|
||||
int(model.ObjectType_collection),
|
||||
int(model.ObjectType_participant),
|
||||
}...),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyIsHidden.String(),
|
||||
Condition: model.BlockContentDataviewFilter_NotEqual,
|
||||
Value: pbtypes.Bool(true),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// prepareQueryFilter combines object name and snippet filters with an OR condition.
|
||||
func (s *SearchService) prepareQueryFilter(searchQuery string) []*model.BlockContentDataviewFilter {
|
||||
if searchQuery == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_Or,
|
||||
NestedFilters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyName.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Like,
|
||||
Value: pbtypes.String(searchQuery),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeySnippet.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Like,
|
||||
Value: pbtypes.String(searchQuery),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// prepareObjectTypeFilters combines object type filters with an OR condition.
|
||||
func (s *SearchService) prepareObjectTypeFilters(spaceId string, objectTypes []string) []*model.BlockContentDataviewFilter {
|
||||
if len(objectTypes) == 0 || objectTypes[0] == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prepare nested filters for each object type
|
||||
nestedFilters := make([]*model.BlockContentDataviewFilter, 0, len(objectTypes))
|
||||
for _, objectType := range objectTypes {
|
||||
typeId := objectType
|
||||
|
||||
if strings.HasPrefix(objectType, "ot-") {
|
||||
var err error
|
||||
typeId, err = util.ResolveUniqueKeyToTypeId(s.mw, spaceId, objectType)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
nestedFilters = append(nestedFilters, &model.BlockContentDataviewFilter{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyType.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(typeId),
|
||||
})
|
||||
}
|
||||
|
||||
if len(nestedFilters) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Combine all filters with an OR operator
|
||||
return []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_Or,
|
||||
NestedFilters: nestedFilters,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// prepareSorts returns a sort filter based on the given sort parameters
|
||||
func (s *SearchService) prepareSorts(sort SortOptions) *model.BlockContentDataviewSort {
|
||||
return &model.BlockContentDataviewSort{
|
||||
RelationKey: s.getSortRelationKey(sort.Timestamp),
|
||||
Type: s.getSortDirection(sort.Direction),
|
||||
Format: model.RelationFormat_date,
|
||||
IncludeTime: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
|
||||
}
|
||||
}
|
||||
|
||||
// getSortRelationKey returns the relation key for the given sort timestamp
|
||||
func (s *SearchService) getSortRelationKey(timestamp string) string {
|
||||
switch timestamp {
|
||||
case "created_date":
|
||||
return bundle.RelationKeyCreatedDate.String()
|
||||
case "last_modified_date":
|
||||
return bundle.RelationKeyLastModifiedDate.String()
|
||||
case "last_opened_date":
|
||||
return bundle.RelationKeyLastOpenedDate.String()
|
||||
default:
|
||||
return bundle.RelationKeyLastModifiedDate.String()
|
||||
}
|
||||
}
|
||||
|
||||
// getSortDirection returns the sort direction for the given string
|
||||
func (s *SearchService) getSortDirection(direction string) model.BlockContentDataviewSortType {
|
||||
switch direction {
|
||||
case "asc":
|
||||
return model.BlockContentDataviewSort_Asc
|
||||
case "desc":
|
||||
return model.BlockContentDataviewSort_Desc
|
||||
default:
|
||||
return model.BlockContentDataviewSort_Desc
|
||||
}
|
||||
}
|
596
core/api/internal/search/service_test.go
Normal file
596
core/api/internal/search/service_test.go
Normal file
|
@ -0,0 +1,596 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/object"
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/space"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service/mock_service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
const (
|
||||
offset = 0
|
||||
limit = 100
|
||||
techSpaceId = "tech-space-id"
|
||||
gatewayUrl = "http://localhost:31006"
|
||||
mockedSpaceId = "mocked-space-id"
|
||||
mockedSearchTerm = "mocked-search-term"
|
||||
mockedObjectId = "mocked-object-id"
|
||||
mockedObjectName = "mocked-object-name"
|
||||
mockedRootId = "mocked-root-id"
|
||||
mockedParticipantId = "mocked-participant-id"
|
||||
mockedType = "mocked-type"
|
||||
mockedTagId1 = "mocked-tag-id-1"
|
||||
mockedTagValue1 = "mocked-tag-value-1"
|
||||
mockedTagColor1 = "mocked-tag-color-1"
|
||||
mockedTagId2 = "mocked-tag-id-2"
|
||||
mockedTagValue2 = "mocked-tag-value-2"
|
||||
mockedTagColor2 = "mocked-tag-color-2"
|
||||
mockedObjectTypeName = "mocked-object-type-name"
|
||||
mockedParticipantName = "mocked-participant-name"
|
||||
mockedParticipantIcon = "mocked-participant-icon"
|
||||
mockedParticipantImage = "mocked-participant-image"
|
||||
mockedParticipantIdentity = "mocked-participant-identity"
|
||||
mockedParticipantGlobalName = "mocked-participant-global-name"
|
||||
)
|
||||
|
||||
type fixture struct {
|
||||
*SearchService
|
||||
mwMock *mock_service.MockClientCommandsServer
|
||||
}
|
||||
|
||||
func newFixture(t *testing.T) *fixture {
|
||||
mw := mock_service.NewMockClientCommandsServer(t)
|
||||
|
||||
spaceService := space.NewService(mw)
|
||||
spaceService.AccountInfo = &model.AccountInfo{TechSpaceId: techSpaceId, GatewayUrl: gatewayUrl}
|
||||
objectService := object.NewService(mw, spaceService)
|
||||
objectService.AccountInfo = &model.AccountInfo{TechSpaceId: techSpaceId}
|
||||
searchService := NewService(mw, spaceService, objectService)
|
||||
searchService.AccountInfo = &model.AccountInfo{
|
||||
TechSpaceId: techSpaceId,
|
||||
GatewayUrl: gatewayUrl,
|
||||
}
|
||||
|
||||
return &fixture{
|
||||
SearchService: searchService,
|
||||
mwMock: mw,
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchService_GlobalSearch(t *testing.T) {
|
||||
t.Run("objects found globally", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
// Mock retrieving spaces first
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: techSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.ObjectType_spaceView)),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeySpaceLocalStatus.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)),
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{
|
||||
{
|
||||
RelationKey: bundle.RelationKeySpaceOrder.String(),
|
||||
Type: model.BlockContentDataviewSort_Asc,
|
||||
NoCollate: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_End,
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyTargetSpaceId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String()},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyTargetSpaceId.String(): pbtypes.String(mockedSpaceId),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock workspace opening
|
||||
fx.mwMock.On("WorkspaceOpen", mock.Anything, &pb.RpcWorkspaceOpenRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
WithChat: true,
|
||||
}).Return(&pb.RpcWorkspaceOpenResponse{
|
||||
Info: &model.AccountInfo{
|
||||
TechSpaceId: mockedSpaceId,
|
||||
},
|
||||
Error: &pb.RpcWorkspaceOpenResponseError{Code: pb.RpcWorkspaceOpenResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock objects in space
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_And,
|
||||
NestedFilters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_In,
|
||||
Value: pbtypes.IntList([]int{
|
||||
int(model.ObjectType_basic),
|
||||
int(model.ObjectType_profile),
|
||||
int(model.ObjectType_todo),
|
||||
int(model.ObjectType_note),
|
||||
int(model.ObjectType_bookmark),
|
||||
int(model.ObjectType_set),
|
||||
int(model.ObjectType_collection),
|
||||
int(model.ObjectType_participant),
|
||||
}...),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyIsHidden.String(),
|
||||
Condition: model.BlockContentDataviewFilter_NotEqual,
|
||||
Value: pbtypes.Bool(true),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_Or,
|
||||
NestedFilters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyName.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Like,
|
||||
Value: pbtypes.String(mockedSearchTerm),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeySnippet.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Like,
|
||||
Value: pbtypes.String(mockedSearchTerm),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{{
|
||||
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
|
||||
Type: model.BlockContentDataviewSort_Desc,
|
||||
Format: model.RelationFormat_date,
|
||||
IncludeTime: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
|
||||
}},
|
||||
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeySpaceId.String(), bundle.RelationKeyLastModifiedDate.String()},
|
||||
Limit: int32(offset + limit),
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
|
||||
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock object show for object blocks and details
|
||||
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
ObjectId: mockedObjectId,
|
||||
}).Return(&pb.RpcObjectShowResponse{
|
||||
ObjectView: &model.ObjectView{
|
||||
RootId: mockedRootId,
|
||||
Blocks: []*model.Block{
|
||||
{
|
||||
Id: mockedRootId,
|
||||
Restrictions: &model.BlockRestrictions{
|
||||
Read: false,
|
||||
Edit: false,
|
||||
Remove: false,
|
||||
Drag: false,
|
||||
DropOn: false,
|
||||
},
|
||||
ChildrenIds: []string{"header", "text-block"},
|
||||
},
|
||||
{
|
||||
Id: "header",
|
||||
Restrictions: &model.BlockRestrictions{
|
||||
Read: false,
|
||||
Edit: true,
|
||||
Remove: true,
|
||||
Drag: true,
|
||||
DropOn: true,
|
||||
},
|
||||
ChildrenIds: []string{"title", "featuredRelations"},
|
||||
},
|
||||
{
|
||||
Id: "text-block",
|
||||
Content: &model.BlockContentOfText{
|
||||
Text: &model.BlockContentText{
|
||||
Text: "This is a sample text block",
|
||||
Style: model.BlockContentText_Paragraph,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Details: []*model.ObjectViewDetailsSet{
|
||||
{
|
||||
Id: mockedRootId,
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
|
||||
bundle.RelationKeyLayout.String(): pbtypes.Int64(int64(model.ObjectType_basic)),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String("🌐"),
|
||||
bundle.RelationKeyLastModifiedDate.String(): pbtypes.Float64(999999),
|
||||
bundle.RelationKeyLastModifiedBy.String(): pbtypes.String(mockedParticipantId),
|
||||
bundle.RelationKeyCreatedDate.String(): pbtypes.Float64(888888),
|
||||
bundle.RelationKeyCreator.String(): pbtypes.String(mockedParticipantId),
|
||||
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
|
||||
bundle.RelationKeyType.String(): pbtypes.String(mockedType),
|
||||
bundle.RelationKeyTag.String(): pbtypes.StringList([]string{mockedTagId1, mockedTagId2}),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: mockedParticipantId,
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedParticipantId),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: mockedTagId1,
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedTagValue1),
|
||||
bundle.RelationKeyRelationOptionColor.String(): pbtypes.String(mockedTagColor1),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: mockedTagId2,
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedTagValue2),
|
||||
bundle.RelationKeyRelationOptionColor.String(): pbtypes.String(mockedTagColor2),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
|
||||
}, nil).Once()
|
||||
|
||||
// Mock type resolution
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyId.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(mockedType),
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyName.String()},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectTypeName),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock participant details
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyId.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(mockedParticipantId),
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyId.String(),
|
||||
bundle.RelationKeyName.String(),
|
||||
bundle.RelationKeyIconEmoji.String(),
|
||||
bundle.RelationKeyIconImage.String(),
|
||||
bundle.RelationKeyIdentity.String(),
|
||||
bundle.RelationKeyGlobalName.String(),
|
||||
bundle.RelationKeyParticipantPermissions.String(),
|
||||
},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedParticipantId),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedParticipantName),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String(mockedParticipantIcon),
|
||||
bundle.RelationKeyIconImage.String(): pbtypes.String(mockedParticipantImage),
|
||||
bundle.RelationKeyIdentity.String(): pbtypes.String(mockedParticipantIdentity),
|
||||
bundle.RelationKeyGlobalName.String(): pbtypes.String(mockedParticipantGlobalName),
|
||||
bundle.RelationKeyParticipantPermissions.String(): pbtypes.Int64(int64(model.ParticipantPermissions_Reader)),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Twice()
|
||||
|
||||
// when
|
||||
objects, total, hasMore, err := fx.GlobalSearch(ctx, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Direction: "desc", Timestamp: "last_modified_date"}}, offset, limit)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Len(t, objects, 1)
|
||||
require.Equal(t, mockedObjectTypeName, objects[0].Type)
|
||||
require.Equal(t, mockedSpaceId, objects[0].SpaceId)
|
||||
require.Equal(t, mockedObjectName, objects[0].Name)
|
||||
require.Equal(t, mockedObjectId, objects[0].Id)
|
||||
require.Equal(t, model.ObjectTypeLayout_name[int32(model.ObjectType_basic)], objects[0].Layout)
|
||||
require.Equal(t, "🌐", objects[0].Icon)
|
||||
require.Equal(t, "This is a sample text block", objects[0].Blocks[2].Text.Text)
|
||||
|
||||
// check details
|
||||
for _, detail := range objects[0].Details {
|
||||
if detail.Id == "created_date" {
|
||||
require.Equal(t, "1970-01-11T06:54:48Z", detail.Details["created_date"])
|
||||
} else if detail.Id == "last_modified_date" {
|
||||
require.Equal(t, "1970-01-12T13:46:39Z", detail.Details["last_modified_date"])
|
||||
} else if detail.Id == "created_by" {
|
||||
require.Equal(t, mockedParticipantId, detail.Details["details"].(space.Member).Id)
|
||||
require.Equal(t, mockedParticipantName, detail.Details["details"].(space.Member).Name)
|
||||
require.Equal(t, gatewayUrl+"/image/"+mockedParticipantImage, detail.Details["details"].(space.Member).Icon)
|
||||
require.Equal(t, mockedParticipantIdentity, detail.Details["details"].(space.Member).Identity)
|
||||
require.Equal(t, mockedParticipantGlobalName, detail.Details["details"].(space.Member).GlobalName)
|
||||
} else if detail.Id == "last_modified_by" {
|
||||
require.Equal(t, mockedParticipantId, detail.Details["details"].(space.Member).Id)
|
||||
}
|
||||
}
|
||||
|
||||
// check tags
|
||||
tags := []object.Tag{}
|
||||
for _, detail := range objects[0].Details {
|
||||
if tagList, ok := detail.Details["tags"].([]object.Tag); ok {
|
||||
for _, tag := range tagList {
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
require.Len(t, tags, 2)
|
||||
require.Equal(t, mockedTagId1, tags[0].Id)
|
||||
require.Equal(t, mockedTagValue1, tags[0].Name)
|
||||
require.Equal(t, mockedTagColor1, tags[0].Color)
|
||||
require.Equal(t, mockedTagId2, tags[1].Id)
|
||||
require.Equal(t, mockedTagValue2, tags[1].Name)
|
||||
require.Equal(t, mockedTagColor2, tags[1].Color)
|
||||
|
||||
require.Equal(t, 1, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSearchService_Search(t *testing.T) {
|
||||
t.Run("objects found in a specific space", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
// Mock objects in space
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_And,
|
||||
NestedFilters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_In,
|
||||
Value: pbtypes.IntList([]int{
|
||||
int(model.ObjectType_basic),
|
||||
int(model.ObjectType_profile),
|
||||
int(model.ObjectType_todo),
|
||||
int(model.ObjectType_note),
|
||||
int(model.ObjectType_bookmark),
|
||||
int(model.ObjectType_set),
|
||||
int(model.ObjectType_collection),
|
||||
int(model.ObjectType_participant),
|
||||
}...),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyIsHidden.String(),
|
||||
Condition: model.BlockContentDataviewFilter_NotEqual,
|
||||
Value: pbtypes.Bool(true),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_Or,
|
||||
NestedFilters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyName.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Like,
|
||||
Value: pbtypes.String(mockedSearchTerm),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeySnippet.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Like,
|
||||
Value: pbtypes.String(mockedSearchTerm),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{{
|
||||
RelationKey: bundle.RelationKeyLastModifiedDate.String(),
|
||||
Type: model.BlockContentDataviewSort_Desc,
|
||||
Format: model.RelationFormat_date,
|
||||
IncludeTime: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_NotSpecified,
|
||||
}},
|
||||
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeySpaceId.String(), bundle.RelationKeyLastModifiedDate.String()},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
|
||||
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock object show for object details
|
||||
fx.mwMock.On("ObjectShow", mock.Anything, &pb.RpcObjectShowRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
ObjectId: mockedObjectId,
|
||||
}).Return(&pb.RpcObjectShowResponse{
|
||||
ObjectView: &model.ObjectView{
|
||||
RootId: mockedRootId,
|
||||
Details: []*model.ObjectViewDetailsSet{
|
||||
{
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String(mockedObjectId),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectName),
|
||||
bundle.RelationKeyLayout.String(): pbtypes.Int64(int64(model.ObjectType_basic)),
|
||||
bundle.RelationKeyLastModifiedDate.String(): pbtypes.Float64(999999),
|
||||
bundle.RelationKeySpaceId.String(): pbtypes.String(mockedSpaceId),
|
||||
bundle.RelationKeyType.String(): pbtypes.String(mockedType),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectShowResponseError{Code: pb.RpcObjectShowResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock type resolution
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyId.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(mockedType),
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyName.String()},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String(mockedObjectTypeName),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// Mock participant details
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: mockedSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyId.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(""),
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyId.String(),
|
||||
bundle.RelationKeyName.String(),
|
||||
bundle.RelationKeyIconEmoji.String(),
|
||||
bundle.RelationKeyIconImage.String(),
|
||||
bundle.RelationKeyIdentity.String(),
|
||||
bundle.RelationKeyGlobalName.String(),
|
||||
bundle.RelationKeyParticipantPermissions.String(),
|
||||
},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Twice()
|
||||
|
||||
// when
|
||||
objects, total, hasMore, err := fx.Search(ctx, mockedSpaceId, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Direction: "desc", Timestamp: "last_modified_date"}}, offset, limit)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Len(t, objects, 1)
|
||||
require.Equal(t, mockedObjectName, objects[0].Name)
|
||||
require.Equal(t, mockedObjectId, objects[0].Id)
|
||||
require.Equal(t, mockedSpaceId, objects[0].SpaceId)
|
||||
require.Equal(t, model.ObjectTypeLayout_name[int32(model.ObjectType_basic)], objects[0].Layout)
|
||||
|
||||
require.Equal(t, 1, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
|
||||
t.Run("no objects found in space", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
objects, total, hasMore, err := fx.Search(ctx, mockedSpaceId, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Direction: "desc", Timestamp: "last_modified_date"}}, offset, limit)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Len(t, objects, 0)
|
||||
require.Equal(t, 0, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
|
||||
t.Run("error during search", func(t *testing.T) {
|
||||
// given
|
||||
ctx := context.Background()
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).Return(&pb.RpcObjectSearchResponse{
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_UNKNOWN_ERROR},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
objects, total, hasMore, err := fx.Search(ctx, mockedSpaceId, SearchRequest{Query: mockedSearchTerm, Types: []string{}, Sort: SortOptions{Direction: "desc", Timestamp: "last_modified_date"}}, offset, limit)
|
||||
|
||||
// then
|
||||
require.Error(t, err)
|
||||
require.Empty(t, objects)
|
||||
require.Equal(t, 0, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
}
|
113
core/api/internal/space/handler.go
Normal file
113
core/api/internal/space/handler.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package space
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/core/api/util"
|
||||
)
|
||||
|
||||
// GetSpacesHandler retrieves a list of spaces
|
||||
//
|
||||
// @Summary List spaces
|
||||
// @Tags spaces
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
|
||||
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
|
||||
// @Success 200 {object} pagination.PaginatedResponse[Space] "List of spaces"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces [get]
|
||||
func GetSpacesHandler(s *SpaceService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
spaces, total, hasMore, err := s.ListSpaces(c.Request.Context(), offset, limit)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrFailedListSpaces, http.StatusInternalServerError),
|
||||
util.ErrToCode(ErrFailedOpenWorkspace, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, spaces, total, offset, limit, hasMore)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateSpaceHandler creates a new space
|
||||
//
|
||||
// @Summary Create space
|
||||
// @Tags spaces
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param name body CreateSpaceRequest true "Space to create"
|
||||
// @Success 200 {object} CreateSpaceResponse "Space created successfully"
|
||||
// @Failure 400 {object} util.ValidationError "Bad request"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces [post]
|
||||
func CreateSpaceHandler(s *SpaceService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
nameRequest := CreateSpaceRequest{}
|
||||
if err := c.BindJSON(&nameRequest); err != nil {
|
||||
apiErr := util.CodeToAPIError(http.StatusBadRequest, err.Error())
|
||||
c.JSON(http.StatusBadRequest, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
space, err := s.CreateSpace(c.Request.Context(), nameRequest.Name)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrFailedCreateSpace, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, CreateSpaceResponse{Space: space})
|
||||
}
|
||||
}
|
||||
|
||||
// GetMembersHandler retrieves a list of members in a space
|
||||
//
|
||||
// @Summary List members
|
||||
// @Tags spaces
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param space_id path string true "Space ID"
|
||||
// @Param offset query int false "The number of items to skip before starting to collect the result set" default(0)
|
||||
// @Param limit query int false "The number of items to return" default(100) maximum(1000)
|
||||
// @Success 200 {object} pagination.PaginatedResponse[Member] "List of members"
|
||||
// @Failure 401 {object} util.UnauthorizedError "Unauthorized"
|
||||
// @Failure 500 {object} util.ServerError "Internal server error"
|
||||
// @Router /spaces/{space_id}/members [get]
|
||||
func GetMembersHandler(s *SpaceService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
spaceId := c.Param("space_id")
|
||||
offset := c.GetInt("offset")
|
||||
limit := c.GetInt("limit")
|
||||
|
||||
members, total, hasMore, err := s.ListMembers(c.Request.Context(), spaceId, offset, limit)
|
||||
code := util.MapErrorCode(err,
|
||||
util.ErrToCode(ErrFailedListMembers, http.StatusInternalServerError),
|
||||
)
|
||||
|
||||
if code != http.StatusOK {
|
||||
apiErr := util.CodeToAPIError(code, err.Error())
|
||||
c.JSON(code, apiErr)
|
||||
return
|
||||
}
|
||||
|
||||
pagination.RespondWithPagination(c, http.StatusOK, members, total, offset, limit, hasMore)
|
||||
}
|
||||
}
|
41
core/api/internal/space/model.go
Normal file
41
core/api/internal/space/model.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package space
|
||||
|
||||
type CreateSpaceRequest struct {
|
||||
Name string `json:"name" example:"New Space"`
|
||||
}
|
||||
|
||||
type CreateSpaceResponse struct {
|
||||
Space Space `json:"space"`
|
||||
}
|
||||
|
||||
type Space struct {
|
||||
Type string `json:"type" example:"space"`
|
||||
Id string `json:"id" example:"bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1"`
|
||||
Name string `json:"name" example:"Space Name"`
|
||||
Icon string `json:"icon" example:"http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay"`
|
||||
HomeObjectId string `json:"home_object_id" example:"bafyreie4qcl3wczb4cw5hrfyycikhjyh6oljdis3ewqrk5boaav3sbwqya"`
|
||||
ArchiveObjectId string `json:"archive_object_id" example:"bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri"`
|
||||
ProfileObjectId string `json:"profile_object_id" example:"bafyreiaxhwreshjqwndpwtdsu4mtihaqhhmlygqnyqpfyfwlqfq3rm3gw4"`
|
||||
MarketplaceWorkspaceId string `json:"marketplace_workspace_id" example:"_anytype_marketplace"`
|
||||
WorkspaceObjectId string `json:"workspace_object_id" example:"bafyreiapey2g6e6za4zfxvlgwdy4hbbfu676gmwrhnqvjbxvrchr7elr3y"`
|
||||
DeviceId string `json:"device_id" example:"12D3KooWGZMJ4kQVyQVXaj7gJPZr3RZ2nvd9M2Eq2pprEoPih9WF"`
|
||||
AccountSpaceId string `json:"account_space_id" example:"bafyreihpd2knon5wbljhtfeg3fcqtg3i2pomhhnigui6lrjmzcjzep7gcy.23me69r569oi1"`
|
||||
WidgetsId string `json:"widgets_id" example:"bafyreialj7pceh53mifm5dixlho47ke4qjmsn2uh4wsjf7xq2pnlo5xfva"`
|
||||
SpaceViewId string `json:"space_view_id" example:"bafyreigzv3vq7qwlrsin6njoduq727ssnhwd6bgyfj6nm4hv3pxoc2rxhy"`
|
||||
TechSpaceId string `json:"tech_space_id" example:"bafyreif4xuwncrjl6jajt4zrrfnylpki476nv2w64yf42ovt7gia7oypii.23me69r569oi1"`
|
||||
GatewayUrl string `json:"gateway_url" example:"http://127.0.0.1:31006"`
|
||||
LocalStoragePath string `json:"local_storage_path" example:"/Users/johndoe/Library/Application Support/Anytype/data/AAHTtt1wuQEnaYBNZ2Cyfcvs6DqPqxgn8VXDVk4avsUkMuha"`
|
||||
Timezone string `json:"timezone" example:""`
|
||||
AnalyticsId string `json:"analytics_id" example:"624aecdd-4797-4611-9d61-a2ae5f53cf1c"`
|
||||
NetworkId string `json:"network_id" example:"N83gJpVd9MuNRZAuJLZ7LiMntTThhPc6DtzWWVjb1M3PouVU"`
|
||||
}
|
||||
|
||||
type Member struct {
|
||||
Type string `json:"type" example:"member"`
|
||||
Id string `json:"id" example:"_participant_bafyreigyfkt6rbv24sbv5aq2hko1bhmv5xxlf22b4bypdu6j7hnphm3psq_23me69r569oi1_AAjEaEwPF4nkEh9AWkqEnzcQ8HziBB4ETjiTpvRCQvWnSMDZ"`
|
||||
Name string `json:"name" example:"John Doe"`
|
||||
Icon string `json:"icon" example:"http://127.0.0.1:31006/image/bafybeieptz5hvcy6txplcvphjbbh5yjc2zqhmihs3owkh5oab4ezauzqay?width=100"`
|
||||
Identity string `json:"identity" example:"AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ"`
|
||||
GlobalName string `json:"global_name" example:"john.any"`
|
||||
Role string `json:"role" enums:"Reader,Writer,Owner,NoPermission" example:"Owner"`
|
||||
}
|
242
core/api/internal/space/service.go
Normal file
242
core/api/internal/space/service.go
Normal file
|
@ -0,0 +1,242 @@
|
|||
package space
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/core/api/util"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailedListSpaces = errors.New("failed to retrieve list of spaces")
|
||||
ErrFailedOpenWorkspace = errors.New("failed to open workspace")
|
||||
ErrFailedGenerateRandomIcon = errors.New("failed to generate random icon")
|
||||
ErrFailedCreateSpace = errors.New("failed to create space")
|
||||
ErrFailedListMembers = errors.New("failed to retrieve list of members")
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
ListSpaces(ctx context.Context, offset int, limit int) ([]Space, int, bool, error)
|
||||
CreateSpace(ctx context.Context, name string) (Space, error)
|
||||
ListMembers(ctx context.Context, spaceId string, offset int, limit int) ([]Member, int, bool, error)
|
||||
}
|
||||
|
||||
type SpaceService struct {
|
||||
mw service.ClientCommandsServer
|
||||
AccountInfo *model.AccountInfo
|
||||
}
|
||||
|
||||
func NewService(mw service.ClientCommandsServer) *SpaceService {
|
||||
return &SpaceService{mw: mw}
|
||||
}
|
||||
|
||||
// ListSpaces returns a paginated list of spaces for the account.
|
||||
func (s *SpaceService) ListSpaces(ctx context.Context, offset int, limit int) (spaces []Space, total int, hasMore bool, err error) {
|
||||
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: s.AccountInfo.TechSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.ObjectType_spaceView)),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeySpaceLocalStatus.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)),
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{
|
||||
{
|
||||
RelationKey: bundle.RelationKeySpaceOrder.String(),
|
||||
Type: model.BlockContentDataviewSort_Asc,
|
||||
NoCollate: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_End,
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyTargetSpaceId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String()},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return nil, 0, false, ErrFailedListSpaces
|
||||
}
|
||||
|
||||
total = len(resp.Records)
|
||||
paginatedRecords, hasMore := pagination.Paginate(resp.Records, offset, limit)
|
||||
spaces = make([]Space, 0, len(paginatedRecords))
|
||||
|
||||
for _, record := range paginatedRecords {
|
||||
workspace, err := s.getWorkspaceInfo(record.Fields[bundle.RelationKeyTargetSpaceId.String()].GetStringValue())
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
|
||||
// TODO: name and icon are only returned here; fix that
|
||||
workspace.Name = record.Fields[bundle.RelationKeyName.String()].GetStringValue()
|
||||
workspace.Icon = util.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), record.Fields[bundle.RelationKeyIconImage.String()].GetStringValue())
|
||||
|
||||
spaces = append(spaces, workspace)
|
||||
}
|
||||
|
||||
return spaces, total, hasMore, nil
|
||||
}
|
||||
|
||||
// CreateSpace creates a new space with the given name and returns the space info.
|
||||
func (s *SpaceService) CreateSpace(ctx context.Context, name string) (Space, error) {
|
||||
iconOption, err := rand.Int(rand.Reader, big.NewInt(13))
|
||||
if err != nil {
|
||||
return Space{}, ErrFailedGenerateRandomIcon
|
||||
}
|
||||
|
||||
// Create new workspace with a random icon and import default use case
|
||||
resp := s.mw.WorkspaceCreate(ctx, &pb.RpcWorkspaceCreateRequest{
|
||||
Details: &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyIconOption.String(): pbtypes.Float64(float64(iconOption.Int64())),
|
||||
bundle.RelationKeyName.String(): pbtypes.String(name),
|
||||
bundle.RelationKeySpaceDashboardId.String(): pbtypes.String("lastOpened"),
|
||||
},
|
||||
},
|
||||
UseCase: pb.RpcObjectImportUseCaseRequest_GET_STARTED,
|
||||
WithChat: true,
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcWorkspaceCreateResponseError_NULL {
|
||||
return Space{}, ErrFailedCreateSpace
|
||||
}
|
||||
|
||||
return s.getWorkspaceInfo(resp.SpaceId)
|
||||
}
|
||||
|
||||
// ListMembers returns a paginated list of members in the space with the given ID.
|
||||
func (s *SpaceService) ListMembers(ctx context.Context, spaceId string, offset int, limit int) (members []Member, total int, hasMore bool, err error) {
|
||||
resp := s.mw.ObjectSearch(ctx, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.ObjectType_participant)),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyParticipantStatus.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.ParticipantStatus_Active)),
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyName.String(),
|
||||
Type: model.BlockContentDataviewSort_Asc,
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String(), bundle.RelationKeyIdentity.String(), bundle.RelationKeyGlobalName.String(), bundle.RelationKeyParticipantPermissions.String()},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return nil, 0, false, ErrFailedListMembers
|
||||
}
|
||||
|
||||
total = len(resp.Records)
|
||||
paginatedMembers, hasMore := pagination.Paginate(resp.Records, offset, limit)
|
||||
members = make([]Member, 0, len(paginatedMembers))
|
||||
|
||||
for _, record := range paginatedMembers {
|
||||
icon := util.GetIconFromEmojiOrImage(s.AccountInfo, record.Fields[bundle.RelationKeyIconEmoji.String()].GetStringValue(), record.Fields[bundle.RelationKeyIconImage.String()].GetStringValue())
|
||||
|
||||
member := Member{
|
||||
Type: "member",
|
||||
Id: record.Fields[bundle.RelationKeyId.String()].GetStringValue(),
|
||||
Name: record.Fields[bundle.RelationKeyName.String()].GetStringValue(),
|
||||
Icon: icon,
|
||||
Identity: record.Fields[bundle.RelationKeyIdentity.String()].GetStringValue(),
|
||||
GlobalName: record.Fields[bundle.RelationKeyGlobalName.String()].GetStringValue(),
|
||||
Role: model.ParticipantPermissions_name[int32(record.Fields[bundle.RelationKeyParticipantPermissions.String()].GetNumberValue())],
|
||||
}
|
||||
|
||||
members = append(members, member)
|
||||
}
|
||||
|
||||
return members, total, hasMore, nil
|
||||
}
|
||||
|
||||
func (s *SpaceService) GetParticipantDetails(mw service.ClientCommandsServer, spaceId string, participantId string) Member {
|
||||
resp := mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyId.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(participantId),
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyId.String(), bundle.RelationKeyName.String(), bundle.RelationKeyIconEmoji.String(), bundle.RelationKeyIconImage.String(), bundle.RelationKeyIdentity.String(), bundle.RelationKeyGlobalName.String(), bundle.RelationKeyParticipantPermissions.String()},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return Member{}
|
||||
}
|
||||
|
||||
if len(resp.Records) == 0 {
|
||||
return Member{}
|
||||
}
|
||||
|
||||
icon := util.GetIconFromEmojiOrImage(s.AccountInfo, "", resp.Records[0].Fields[bundle.RelationKeyIconImage.String()].GetStringValue())
|
||||
|
||||
return Member{
|
||||
Type: "member",
|
||||
Id: resp.Records[0].Fields[bundle.RelationKeyId.String()].GetStringValue(),
|
||||
Name: resp.Records[0].Fields[bundle.RelationKeyName.String()].GetStringValue(),
|
||||
Icon: icon,
|
||||
Identity: resp.Records[0].Fields[bundle.RelationKeyIdentity.String()].GetStringValue(),
|
||||
GlobalName: resp.Records[0].Fields[bundle.RelationKeyGlobalName.String()].GetStringValue(),
|
||||
Role: model.ParticipantPermissions_name[int32(resp.Records[0].Fields[bundle.RelationKeyParticipantPermissions.String()].GetNumberValue())],
|
||||
}
|
||||
}
|
||||
|
||||
// getWorkspaceInfo returns the workspace info for the space with the given ID.
|
||||
func (s *SpaceService) getWorkspaceInfo(spaceId string) (space Space, err error) {
|
||||
workspaceResponse := s.mw.WorkspaceOpen(context.Background(), &pb.RpcWorkspaceOpenRequest{
|
||||
SpaceId: spaceId,
|
||||
WithChat: true,
|
||||
})
|
||||
|
||||
if workspaceResponse.Error.Code != pb.RpcWorkspaceOpenResponseError_NULL {
|
||||
return Space{}, ErrFailedOpenWorkspace
|
||||
}
|
||||
|
||||
return Space{
|
||||
Type: "space",
|
||||
Id: spaceId,
|
||||
HomeObjectId: workspaceResponse.Info.HomeObjectId,
|
||||
ArchiveObjectId: workspaceResponse.Info.ArchiveObjectId,
|
||||
ProfileObjectId: workspaceResponse.Info.ProfileObjectId,
|
||||
MarketplaceWorkspaceId: workspaceResponse.Info.MarketplaceWorkspaceId,
|
||||
WorkspaceObjectId: workspaceResponse.Info.WorkspaceObjectId,
|
||||
DeviceId: workspaceResponse.Info.DeviceId,
|
||||
AccountSpaceId: workspaceResponse.Info.AccountSpaceId,
|
||||
WidgetsId: workspaceResponse.Info.WidgetsId,
|
||||
SpaceViewId: workspaceResponse.Info.SpaceViewId,
|
||||
TechSpaceId: workspaceResponse.Info.TechSpaceId,
|
||||
GatewayUrl: workspaceResponse.Info.GatewayUrl,
|
||||
LocalStoragePath: workspaceResponse.Info.LocalStoragePath,
|
||||
Timezone: workspaceResponse.Info.TimeZone,
|
||||
AnalyticsId: workspaceResponse.Info.AnalyticsId,
|
||||
NetworkId: workspaceResponse.Info.NetworkId,
|
||||
}, nil
|
||||
}
|
313
core/api/internal/space/service_test.go
Normal file
313
core/api/internal/space/service_test.go
Normal file
|
@ -0,0 +1,313 @@
|
|||
package space
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gogo/protobuf/types"
|
||||
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service/mock_service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
const (
|
||||
offset = 0
|
||||
limit = 100
|
||||
techSpaceId = "tech-space-id"
|
||||
gatewayUrl = "http://localhost:31006"
|
||||
iconImage = "bafyreialsgoyflf3etjm3parzurivyaukzivwortf32b4twnlwpwocsrri"
|
||||
)
|
||||
|
||||
type fixture struct {
|
||||
*SpaceService
|
||||
mwMock *mock_service.MockClientCommandsServer
|
||||
}
|
||||
|
||||
func newFixture(t *testing.T) *fixture {
|
||||
mw := mock_service.NewMockClientCommandsServer(t)
|
||||
spaceService := NewService(mw)
|
||||
spaceService.AccountInfo = &model.AccountInfo{
|
||||
TechSpaceId: techSpaceId,
|
||||
GatewayUrl: gatewayUrl,
|
||||
}
|
||||
|
||||
return &fixture{
|
||||
SpaceService: spaceService,
|
||||
mwMock: mw,
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpaceService_ListSpaces(t *testing.T) {
|
||||
t.Run("successful retrieval of spaces", func(t *testing.T) {
|
||||
// given
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, &pb.RpcObjectSearchRequest{
|
||||
SpaceId: techSpaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeyLayout.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.ObjectType_spaceView)),
|
||||
},
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: bundle.RelationKeySpaceLocalStatus.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.Int64(int64(model.SpaceStatus_Ok)),
|
||||
},
|
||||
},
|
||||
Sorts: []*model.BlockContentDataviewSort{
|
||||
{
|
||||
RelationKey: "spaceOrder",
|
||||
Type: model.BlockContentDataviewSort_Asc,
|
||||
NoCollate: true,
|
||||
EmptyPlacement: model.BlockContentDataviewSort_End,
|
||||
},
|
||||
},
|
||||
Keys: []string{"targetSpaceId", "name", "iconEmoji", "iconImage"},
|
||||
}).Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String("Another Workspace"),
|
||||
bundle.RelationKeyTargetSpaceId.String(): pbtypes.String("another-space-id"),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String(""),
|
||||
bundle.RelationKeyIconImage.String(): pbtypes.String(iconImage),
|
||||
},
|
||||
},
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String("My Workspace"),
|
||||
bundle.RelationKeyTargetSpaceId.String(): pbtypes.String("my-space-id"),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String("🚀"),
|
||||
bundle.RelationKeyIconImage.String(): pbtypes.String(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
fx.mwMock.On("WorkspaceOpen", mock.Anything, mock.Anything).Return(&pb.RpcWorkspaceOpenResponse{
|
||||
Error: &pb.RpcWorkspaceOpenResponseError{Code: pb.RpcWorkspaceOpenResponseError_NULL},
|
||||
Info: &model.AccountInfo{
|
||||
HomeObjectId: "home-object-id",
|
||||
ArchiveObjectId: "archive-object-id",
|
||||
ProfileObjectId: "profile-object-id",
|
||||
MarketplaceWorkspaceId: "marketplace-workspace-id",
|
||||
WorkspaceObjectId: "workspace-object-id",
|
||||
DeviceId: "device-id",
|
||||
AccountSpaceId: "account-space-id",
|
||||
WidgetsId: "widgets-id",
|
||||
SpaceViewId: "space-view-id",
|
||||
TechSpaceId: "tech-space-id",
|
||||
GatewayUrl: "gateway-url",
|
||||
LocalStoragePath: "local-storage-path",
|
||||
TimeZone: "time-zone",
|
||||
AnalyticsId: "analytics-id",
|
||||
NetworkId: "network-id",
|
||||
},
|
||||
}, nil).Twice()
|
||||
|
||||
// when
|
||||
spaces, total, hasMore, err := fx.ListSpaces(nil, offset, limit)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Len(t, spaces, 2)
|
||||
require.Equal(t, "Another Workspace", spaces[0].Name)
|
||||
require.Equal(t, "another-space-id", spaces[0].Id)
|
||||
require.Regexpf(t, regexp.MustCompile(gatewayUrl+`/image/`+iconImage), spaces[0].Icon, "Icon URL does not match")
|
||||
require.Equal(t, "My Workspace", spaces[1].Name)
|
||||
require.Equal(t, "my-space-id", spaces[1].Id)
|
||||
require.Equal(t, "🚀", spaces[1].Icon)
|
||||
require.Equal(t, 2, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
|
||||
t.Run("no spaces found", func(t *testing.T) {
|
||||
// given
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
spaces, total, hasMore, err := fx.ListSpaces(nil, offset, limit)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Len(t, spaces, 0)
|
||||
require.Equal(t, 0, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
|
||||
t.Run("failed workspace open", func(t *testing.T) {
|
||||
// given
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyName.String(): pbtypes.String("My Workspace"),
|
||||
bundle.RelationKeyTargetSpaceId.String(): pbtypes.String("my-space-id"),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String("🚀"),
|
||||
bundle.RelationKeyIconImage.String(): pbtypes.String(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
fx.mwMock.On("WorkspaceOpen", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcWorkspaceOpenResponse{
|
||||
Error: &pb.RpcWorkspaceOpenResponseError{Code: pb.RpcWorkspaceOpenResponseError_UNKNOWN_ERROR},
|
||||
}, nil).Once()
|
||||
|
||||
// when
|
||||
spaces, total, hasMore, err := fx.ListSpaces(nil, offset, limit)
|
||||
|
||||
// then
|
||||
require.ErrorIs(t, err, ErrFailedOpenWorkspace)
|
||||
require.Len(t, spaces, 0)
|
||||
require.Equal(t, 0, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceService_CreateSpace(t *testing.T) {
|
||||
t.Run("successful create space", func(t *testing.T) {
|
||||
// given
|
||||
fx := newFixture(t)
|
||||
fx.mwMock.On("WorkspaceCreate", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcWorkspaceCreateResponse{
|
||||
Error: &pb.RpcWorkspaceCreateResponseError{Code: pb.RpcWorkspaceCreateResponseError_NULL},
|
||||
SpaceId: "new-space-id",
|
||||
}).Once()
|
||||
|
||||
fx.mwMock.On("WorkspaceOpen", mock.Anything, mock.Anything).Return(&pb.RpcWorkspaceOpenResponse{
|
||||
Error: &pb.RpcWorkspaceOpenResponseError{Code: pb.RpcWorkspaceOpenResponseError_NULL},
|
||||
Info: &model.AccountInfo{
|
||||
HomeObjectId: "home-object-id",
|
||||
ArchiveObjectId: "archive-object-id",
|
||||
ProfileObjectId: "profile-object-id",
|
||||
MarketplaceWorkspaceId: "marketplace-workspace-id",
|
||||
WorkspaceObjectId: "workspace-object-id",
|
||||
DeviceId: "device-id",
|
||||
AccountSpaceId: "account-space-id",
|
||||
WidgetsId: "widgets-id",
|
||||
SpaceViewId: "space-view-id",
|
||||
TechSpaceId: "tech-space-id",
|
||||
GatewayUrl: "gateway-url",
|
||||
LocalStoragePath: "local-storage-path",
|
||||
TimeZone: "time-zone",
|
||||
AnalyticsId: "analytics-id",
|
||||
NetworkId: "network-id",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
// when
|
||||
space, err := fx.CreateSpace(nil, "New Space")
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "new-space-id", space.Id)
|
||||
})
|
||||
|
||||
t.Run("failed workspace creation", func(t *testing.T) {
|
||||
// given
|
||||
fx := newFixture(t)
|
||||
fx.mwMock.On("WorkspaceCreate", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcWorkspaceCreateResponse{
|
||||
Error: &pb.RpcWorkspaceCreateResponseError{Code: pb.RpcWorkspaceCreateResponseError_UNKNOWN_ERROR},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
space, err := fx.CreateSpace(nil, "New Space")
|
||||
|
||||
// then
|
||||
require.ErrorIs(t, err, ErrFailedCreateSpace)
|
||||
require.Equal(t, Space{}, space)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpaceService_ListMembers(t *testing.T) {
|
||||
t.Run("successfully get members", func(t *testing.T) {
|
||||
// given
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String("member-1"),
|
||||
bundle.RelationKeyName.String(): pbtypes.String("John Doe"),
|
||||
bundle.RelationKeyIconEmoji.String(): pbtypes.String("👤"),
|
||||
bundle.RelationKeyIdentity.String(): pbtypes.String("AAjEaEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMDZ"),
|
||||
bundle.RelationKeyGlobalName.String(): pbtypes.String("john.any"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Fields: map[string]*types.Value{
|
||||
bundle.RelationKeyId.String(): pbtypes.String("member-2"),
|
||||
bundle.RelationKeyName.String(): pbtypes.String("Jane Doe"),
|
||||
bundle.RelationKeyIconImage.String(): pbtypes.String(iconImage),
|
||||
bundle.RelationKeyIdentity.String(): pbtypes.String("AAjLbEwPF4nkEh7AWkqEnzcQ8HziGB4ETjiTpvRCQvWnSMD4"),
|
||||
bundle.RelationKeyGlobalName.String(): pbtypes.String("jane.any"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
members, total, hasMore, err := fx.ListMembers(nil, "space-id", offset, limit)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Len(t, members, 2)
|
||||
require.Equal(t, "member-1", members[0].Id)
|
||||
require.Equal(t, "John Doe", members[0].Name)
|
||||
require.Equal(t, "👤", members[0].Icon)
|
||||
require.Equal(t, "john.any", members[0].GlobalName)
|
||||
require.Equal(t, "member-2", members[1].Id)
|
||||
require.Equal(t, "Jane Doe", members[1].Name)
|
||||
require.Regexpf(t, regexp.MustCompile(gatewayUrl+`/image/`+iconImage), members[1].Icon, "Icon URL does not match")
|
||||
require.Equal(t, "jane.any", members[1].GlobalName)
|
||||
require.Equal(t, 2, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
|
||||
t.Run("no members found", func(t *testing.T) {
|
||||
// given
|
||||
fx := newFixture(t)
|
||||
|
||||
fx.mwMock.On("ObjectSearch", mock.Anything, mock.Anything).
|
||||
Return(&pb.RpcObjectSearchResponse{
|
||||
Records: []*types.Struct{},
|
||||
Error: &pb.RpcObjectSearchResponseError{Code: pb.RpcObjectSearchResponseError_NULL},
|
||||
}).Once()
|
||||
|
||||
// when
|
||||
members, total, hasMore, err := fx.ListMembers(nil, "space-id", offset, limit)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Len(t, members, 0)
|
||||
require.Equal(t, 0, total)
|
||||
require.False(t, hasMore)
|
||||
})
|
||||
}
|
13
core/api/pagination/model.go
Normal file
13
core/api/pagination/model.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package pagination
|
||||
|
||||
type PaginationMeta struct {
|
||||
Total int `json:"total" example:"1024"` // the total number of items available on that endpoint
|
||||
Offset int `json:"offset" example:"0"` // the current offset
|
||||
Limit int `json:"limit" example:"100"` // the current limit
|
||||
HasMore bool `json:"has_more" example:"true"` // whether there are more items available
|
||||
}
|
||||
|
||||
type PaginatedResponse[T any] struct {
|
||||
Data []T `json:"data"`
|
||||
Pagination PaginationMeta `json:"pagination"`
|
||||
}
|
82
core/api/pagination/pagination.go
Normal file
82
core/api/pagination/pagination.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package pagination
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Config holds pagination configuration options.
|
||||
type Config struct {
|
||||
DefaultPage int
|
||||
DefaultPageSize int
|
||||
MinPageSize int
|
||||
MaxPageSize int
|
||||
}
|
||||
|
||||
// New creates a Gin middleware for pagination with the provided Config.
|
||||
func New(cfg Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
page := getIntQueryParam(c, "offset", cfg.DefaultPage)
|
||||
size := getIntQueryParam(c, "limit", cfg.DefaultPageSize)
|
||||
|
||||
if size < cfg.MinPageSize || size > cfg.MaxPageSize {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
||||
"error": fmt.Sprintf("limit must be between %d and %d", cfg.MinPageSize, cfg.MaxPageSize),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("offset", page)
|
||||
c.Set("limit", size)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// getIntQueryParam retrieves an integer query parameter or falls back to a default value.
|
||||
func getIntQueryParam(c *gin.Context, key string, defaultValue int) int {
|
||||
valStr := c.DefaultQuery(key, strconv.Itoa(defaultValue))
|
||||
val, err := strconv.Atoi(valStr)
|
||||
if err != nil || val < 0 {
|
||||
return defaultValue
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// RespondWithPagination sends a paginated JSON response.
|
||||
func RespondWithPagination[T any](c *gin.Context, statusCode int, data []T, total int, offset int, limit int, hasMore bool) {
|
||||
c.JSON(statusCode, PaginatedResponse[T]{
|
||||
Data: data,
|
||||
Pagination: PaginationMeta{
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
HasMore: hasMore,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Paginate slices the records based on the offset and limit, and determines if more records are available.
|
||||
func Paginate[T any](records []T, offset int, limit int) ([]T, bool) {
|
||||
if offset < 0 || limit < 1 {
|
||||
return []T{}, len(records) > 0
|
||||
}
|
||||
|
||||
total := len(records)
|
||||
if offset > total {
|
||||
offset = total
|
||||
}
|
||||
|
||||
end := offset + limit
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
paginated := records[offset:end]
|
||||
hasMore := end < total
|
||||
|
||||
return paginated, hasMore
|
||||
}
|
254
core/api/pagination/pagination_test.go
Normal file
254
core/api/pagination/pagination_test.go
Normal file
|
@ -0,0 +1,254 @@
|
|||
package pagination
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
commonConfig := Config{
|
||||
DefaultPage: 0,
|
||||
DefaultPageSize: 10,
|
||||
MinPageSize: 1,
|
||||
MaxPageSize: 50,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
queryParams map[string]string
|
||||
overrideConfig func(cfg Config) Config
|
||||
expectedStatus int
|
||||
expectedOffset int
|
||||
expectedLimit int
|
||||
}{
|
||||
{
|
||||
name: "Valid offset and limit",
|
||||
queryParams: map[string]string{
|
||||
"offset": "10",
|
||||
"limit": "20",
|
||||
},
|
||||
overrideConfig: nil,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedOffset: 10,
|
||||
expectedLimit: 20,
|
||||
},
|
||||
{
|
||||
name: "Offset missing, use default",
|
||||
queryParams: map[string]string{
|
||||
"limit": "20",
|
||||
},
|
||||
overrideConfig: nil,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedOffset: 0,
|
||||
expectedLimit: 20,
|
||||
},
|
||||
{
|
||||
name: "Limit missing, use default",
|
||||
queryParams: map[string]string{
|
||||
"offset": "5",
|
||||
},
|
||||
overrideConfig: nil,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedOffset: 5,
|
||||
expectedLimit: 10,
|
||||
},
|
||||
{
|
||||
name: "Limit below minimum",
|
||||
queryParams: map[string]string{
|
||||
"offset": "5",
|
||||
"limit": "0",
|
||||
},
|
||||
overrideConfig: nil,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Limit above maximum",
|
||||
queryParams: map[string]string{
|
||||
"offset": "5",
|
||||
"limit": "100",
|
||||
},
|
||||
overrideConfig: nil,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Negative offset, use default",
|
||||
queryParams: map[string]string{
|
||||
"offset": "-5",
|
||||
"limit": "10",
|
||||
},
|
||||
overrideConfig: nil,
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedOffset: 0,
|
||||
expectedLimit: 10,
|
||||
},
|
||||
{
|
||||
name: "Custom min and max page size",
|
||||
queryParams: map[string]string{
|
||||
"offset": "5",
|
||||
"limit": "15",
|
||||
},
|
||||
overrideConfig: func(cfg Config) Config {
|
||||
cfg.MinPageSize = 10
|
||||
cfg.MaxPageSize = 20
|
||||
return cfg
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedOffset: 5,
|
||||
expectedLimit: 15,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Apply overrideConfig if provided
|
||||
cfg := commonConfig
|
||||
if tt.overrideConfig != nil {
|
||||
cfg = tt.overrideConfig(cfg)
|
||||
}
|
||||
|
||||
// Set up Gin
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
r.Use(New(cfg))
|
||||
|
||||
// Define a test endpoint
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
offset, _ := c.Get("offset")
|
||||
limit, _ := c.Get("limit")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
})
|
||||
})
|
||||
|
||||
// Create a test request
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
q := req.URL.Query()
|
||||
for k, v := range tt.queryParams {
|
||||
q.Add(k, v)
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
// Perform the request
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
// Check the response
|
||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
||||
|
||||
if w.Code == http.StatusOK {
|
||||
var resp map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Validate offset and limit
|
||||
if offset, ok := resp["offset"].(float64); ok {
|
||||
assert.Equal(t, float64(tt.expectedOffset), offset)
|
||||
}
|
||||
if limit, ok := resp["limit"].(float64); ok {
|
||||
assert.Equal(t, float64(tt.expectedLimit), limit)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaginate(t *testing.T) {
|
||||
type args struct {
|
||||
records []int
|
||||
offset int
|
||||
limit int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantPaginated []int
|
||||
wantHasMore bool
|
||||
}{
|
||||
{
|
||||
name: "Offset=0, Limit=2 (first two items)",
|
||||
args: args{
|
||||
records: []int{1, 2, 3, 4, 5},
|
||||
offset: 0,
|
||||
limit: 2,
|
||||
},
|
||||
wantPaginated: []int{1, 2},
|
||||
wantHasMore: true, // items remain: [3,4,5]
|
||||
},
|
||||
{
|
||||
name: "Offset=2, Limit=2 (middle slice)",
|
||||
args: args{
|
||||
records: []int{1, 2, 3, 4, 5},
|
||||
offset: 2,
|
||||
limit: 2,
|
||||
},
|
||||
wantPaginated: []int{3, 4},
|
||||
wantHasMore: true, // item 5 remains
|
||||
},
|
||||
{
|
||||
name: "Offset=4, Limit=2 (tail of the slice)",
|
||||
args: args{
|
||||
records: []int{1, 2, 3, 4, 5},
|
||||
offset: 4,
|
||||
limit: 2,
|
||||
},
|
||||
wantPaginated: []int{5},
|
||||
wantHasMore: false,
|
||||
},
|
||||
{
|
||||
name: "Offset > length (should return empty)",
|
||||
args: args{
|
||||
records: []int{1, 2, 3, 4, 5},
|
||||
offset: 10,
|
||||
limit: 2,
|
||||
},
|
||||
wantPaginated: []int{},
|
||||
wantHasMore: false,
|
||||
},
|
||||
{
|
||||
name: "Limit > length (should return entire slice)",
|
||||
args: args{
|
||||
records: []int{1, 2, 3},
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
},
|
||||
wantPaginated: []int{1, 2, 3},
|
||||
wantHasMore: false,
|
||||
},
|
||||
{
|
||||
name: "Zero limit (no items returned)",
|
||||
args: args{
|
||||
records: []int{1, 2, 3, 4, 5},
|
||||
offset: 1,
|
||||
limit: 0,
|
||||
},
|
||||
wantPaginated: []int{},
|
||||
wantHasMore: true, // items remain: [2,3,4,5]
|
||||
},
|
||||
{
|
||||
name: "Negative offset and limit (should return empty)",
|
||||
args: args{
|
||||
records: []int{1, 2, 3, 4, 5},
|
||||
offset: -1,
|
||||
limit: -1,
|
||||
},
|
||||
wantPaginated: []int{},
|
||||
wantHasMore: true, // items remain: [1,2,3,4,5]
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotPaginated, gotHasMore := Paginate(tt.args.records, tt.args.offset, tt.args.limit)
|
||||
|
||||
assert.Equal(t, tt.wantPaginated, gotPaginated, "Paginate() gotPaginated = %v, want %v", gotPaginated, tt.wantPaginated)
|
||||
assert.Equal(t, tt.wantHasMore, gotHasMore, "Paginate() gotHasMore = %v, want %v", gotHasMore, tt.wantHasMore)
|
||||
})
|
||||
}
|
||||
}
|
92
core/api/server/middleware.go
Normal file
92
core/api/server/middleware.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/didip/tollbooth/v8"
|
||||
"github.com/didip/tollbooth/v8/limiter"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/anytype/account"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
)
|
||||
|
||||
// rateLimit is a middleware that limits the number of requests per second.
|
||||
func (s *Server) rateLimit(max float64) gin.HandlerFunc {
|
||||
lmt := tollbooth.NewLimiter(max, nil)
|
||||
lmt.SetIPLookup(limiter.IPLookup{
|
||||
Name: "RemoteAddr",
|
||||
IndexFromRight: 0,
|
||||
})
|
||||
|
||||
return func(c *gin.Context) {
|
||||
httpError := tollbooth.LimitByRequest(lmt, c.Writer, c.Request)
|
||||
if httpError != nil {
|
||||
c.AbortWithStatusJSON(httpError.StatusCode, gin.H{"error": httpError.Message})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// ensureAuthenticated is a middleware that ensures the request is authenticated.
|
||||
func (s *Server) ensureAuthenticated(mw service.ClientCommandsServer) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization header"})
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"})
|
||||
return
|
||||
}
|
||||
key := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// Validate the key - if the key exists in the KeyToToken map, it is considered valid.
|
||||
// Otherwise, attempt to create a new session using the key and add it to the map upon successful validation.
|
||||
s.mu.Lock()
|
||||
token, exists := s.KeyToToken[key]
|
||||
s.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
response := mw.WalletCreateSession(context.Background(), &pb.RpcWalletCreateSessionRequest{Auth: &pb.RpcWalletCreateSessionRequestAuthOfAppKey{AppKey: key}})
|
||||
if response.Error.Code != pb.RpcWalletCreateSessionResponseError_NULL {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||
return
|
||||
}
|
||||
token = response.Token
|
||||
|
||||
s.mu.Lock()
|
||||
s.KeyToToken[key] = token
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Add token to request context for downstream services (subscriptions, events, etc.)
|
||||
c.Set("token", token)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// ensureAccountInfo is a middleware that ensures the account info is available in the services.
|
||||
func (s *Server) ensureAccountInfo(accountService account.Service) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
accInfo, err := accountService.GetInfo(context.Background())
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get account info: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
s.exportService.AccountInfo = accInfo
|
||||
s.objectService.AccountInfo = accInfo
|
||||
s.spaceService.AccountInfo = accInfo
|
||||
s.searchService.AccountInfo = accInfo
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
92
core/api/server/router.go
Normal file
92
core/api/server/router.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
|
||||
_ "github.com/anyproto/anytype-heart/core/api/docs"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/anytype/account"
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/auth"
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/export"
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/object"
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/search"
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/space"
|
||||
"github.com/anyproto/anytype-heart/core/api/pagination"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPage = 0
|
||||
defaultPageSize = 100
|
||||
minPageSize = 1
|
||||
maxPageSize = 1000
|
||||
maxWriteRequestsPerSecond = 1
|
||||
)
|
||||
|
||||
// NewRouter builds and returns a *gin.Engine with all routes configured.
|
||||
func (s *Server) NewRouter(accountService account.Service, mw service.ClientCommandsServer) *gin.Engine {
|
||||
debug := os.Getenv("ANYTYPE_API_DEBUG") == "1"
|
||||
if !debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
|
||||
if debug {
|
||||
router.Use(gin.Logger())
|
||||
}
|
||||
paginator := pagination.New(pagination.Config{
|
||||
DefaultPage: defaultPage,
|
||||
DefaultPageSize: defaultPageSize,
|
||||
MinPageSize: minPageSize,
|
||||
MaxPageSize: maxPageSize,
|
||||
})
|
||||
|
||||
// Swagger route
|
||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
// Auth routes (no authentication required)
|
||||
authGroup := router.Group("/v1/auth")
|
||||
{
|
||||
authGroup.POST("/display_code", auth.DisplayCodeHandler(s.authService))
|
||||
authGroup.POST("/token", auth.TokenHandler(s.authService))
|
||||
}
|
||||
|
||||
// API routes
|
||||
v1 := router.Group("/v1")
|
||||
v1.Use(paginator)
|
||||
v1.Use(s.ensureAuthenticated(mw))
|
||||
v1.Use(s.ensureAccountInfo(accountService))
|
||||
{
|
||||
// Export
|
||||
v1.POST("/spaces/:space_id/objects/:object_id/export/:format", export.GetObjectExportHandler(s.exportService))
|
||||
|
||||
// Object
|
||||
v1.GET("/spaces/:space_id/objects", object.GetObjectsHandler(s.objectService))
|
||||
v1.GET("/spaces/:space_id/objects/:object_id", object.GetObjectHandler(s.objectService))
|
||||
v1.DELETE("/spaces/:space_id/objects/:object_id", s.rateLimit(maxWriteRequestsPerSecond), object.DeleteObjectHandler(s.objectService))
|
||||
v1.POST("/spaces/:space_id/objects", s.rateLimit(maxWriteRequestsPerSecond), object.CreateObjectHandler(s.objectService))
|
||||
|
||||
// Search
|
||||
v1.POST("/search", search.GlobalSearchHandler(s.searchService))
|
||||
v1.POST("/spaces/:space_id/search", search.SearchHandler(s.searchService))
|
||||
|
||||
// Space
|
||||
v1.GET("/spaces", space.GetSpacesHandler(s.spaceService))
|
||||
v1.GET("/spaces/:space_id/members", space.GetMembersHandler(s.spaceService))
|
||||
v1.POST("/spaces", s.rateLimit(maxWriteRequestsPerSecond), space.CreateSpaceHandler(s.spaceService))
|
||||
|
||||
// Type
|
||||
v1.GET("/spaces/:space_id/types", object.GetTypesHandler(s.objectService))
|
||||
v1.GET("/spaces/:space_id/types/:type_id", object.GetTypeHandler(s.objectService))
|
||||
v1.GET("/spaces/:space_id/types/:type_id/templates", object.GetTemplatesHandler(s.objectService))
|
||||
v1.GET("/spaces/:space_id/types/:type_id/templates/:template_id", object.GetTemplateHandler(s.objectService))
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
50
core/api/server/server.go
Normal file
50
core/api/server/server.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/anytype/account"
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/auth"
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/export"
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/object"
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/search"
|
||||
"github.com/anyproto/anytype-heart/core/api/internal/space"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
)
|
||||
|
||||
// Server wraps the HTTP server and service logic.
|
||||
type Server struct {
|
||||
engine *gin.Engine
|
||||
|
||||
authService *auth.AuthService
|
||||
exportService *export.ExportService
|
||||
objectService *object.ObjectService
|
||||
spaceService *space.SpaceService
|
||||
searchService *search.SearchService
|
||||
|
||||
mu sync.Mutex
|
||||
KeyToToken map[string]string // appKey -> token
|
||||
}
|
||||
|
||||
// NewServer constructs a new Server with default config and sets up the routes.
|
||||
func NewServer(accountService account.Service, mw service.ClientCommandsServer) *Server {
|
||||
s := &Server{
|
||||
authService: auth.NewService(mw),
|
||||
exportService: export.NewService(mw),
|
||||
spaceService: space.NewService(mw),
|
||||
}
|
||||
|
||||
s.objectService = object.NewService(mw, s.spaceService)
|
||||
s.searchService = search.NewService(mw, s.spaceService, s.objectService)
|
||||
s.engine = s.NewRouter(accountService, mw)
|
||||
s.KeyToToken = make(map[string]string)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Engine returns the underlying gin.Engine.
|
||||
func (s *Server) Engine() *gin.Engine {
|
||||
return s.engine
|
||||
}
|
135
core/api/service.go
Normal file
135
core/api/service.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/app"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/anytype/account"
|
||||
"github.com/anyproto/anytype-heart/core/anytype/config"
|
||||
"github.com/anyproto/anytype-heart/core/api/server"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
)
|
||||
|
||||
const (
|
||||
CName = "api"
|
||||
readTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
mwSrv service.ClientCommandsServer
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
app.ComponentRunnable
|
||||
ReassignAddress(ctx context.Context, listenAddr string) (err error)
|
||||
}
|
||||
|
||||
type apiService struct {
|
||||
srv *server.Server
|
||||
httpSrv *http.Server
|
||||
mw service.ClientCommandsServer
|
||||
accountService account.Service
|
||||
listenAddr string
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func New() Service {
|
||||
return &apiService{mw: mwSrv}
|
||||
}
|
||||
|
||||
func (s *apiService) Name() (name string) {
|
||||
return CName
|
||||
}
|
||||
|
||||
// Init initializes the API service.
|
||||
//
|
||||
// @title Anytype API
|
||||
// @version 1.0
|
||||
// @description This API allows interaction with Anytype resources such as spaces, objects and types.
|
||||
// @termsOfService https://anytype.io/terms_of_use
|
||||
// @contact.name Anytype Support
|
||||
// @contact.url https://anytype.io/contact
|
||||
// @contact.email support@anytype.io
|
||||
// @license.name Any Source Available License 1.0
|
||||
// @license.url https://github.com/anyproto/anytype-ts/blob/main/LICENSE.md
|
||||
// @host localhost:31009
|
||||
// @BasePath /v1
|
||||
// @securityDefinitions.basic BasicAuth
|
||||
// @externalDocs.description OpenAPI
|
||||
// @externalDocs.url https://swagger.io/resources/open-api/
|
||||
func (s *apiService) Init(a *app.App) (err error) {
|
||||
s.listenAddr = a.MustComponent(config.CName).(*config.Config).JsonApiListenAddr
|
||||
s.accountService = a.MustComponent(account.CName).(account.Service)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *apiService) Run(ctx context.Context) (err error) {
|
||||
s.runServer()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *apiService) Close(ctx context.Context) (err error) {
|
||||
return s.shutdown(ctx)
|
||||
}
|
||||
|
||||
func (s *apiService) runServer() {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if s.listenAddr == "" {
|
||||
// means that API is disabled
|
||||
return
|
||||
}
|
||||
|
||||
s.srv = server.NewServer(s.accountService, s.mw)
|
||||
s.httpSrv = &http.Server{
|
||||
Addr: s.listenAddr,
|
||||
Handler: s.srv.Engine(),
|
||||
ReadHeaderTimeout: readTimeout,
|
||||
}
|
||||
|
||||
fmt.Printf("Starting API server on %s\n", s.httpSrv.Addr)
|
||||
|
||||
go func() {
|
||||
if err := s.httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
fmt.Printf("API server ListenAndServe error: %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *apiService) shutdown(ctx context.Context) (err error) {
|
||||
if s.httpSrv == nil {
|
||||
return nil
|
||||
}
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
// we don't want graceful shutdown here and block the app close
|
||||
shutdownCtx, cancel := context.WithTimeout(ctx, time.Millisecond)
|
||||
defer cancel()
|
||||
if err := s.httpSrv.Shutdown(shutdownCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *apiService) ReassignAddress(ctx context.Context, listenAddr string) (err error) {
|
||||
err = s.shutdown(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.listenAddr = listenAddr
|
||||
s.runServer()
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetMiddlewareParams(mw service.ClientCommandsServer) {
|
||||
mwSrv = mw
|
||||
}
|
111
core/api/util/error.go
Normal file
111
core/api/util/error.go
Normal file
|
@ -0,0 +1,111 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// 400
|
||||
type ValidationError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// 401
|
||||
type UnauthorizedError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// 403
|
||||
type ForbiddenError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// 404
|
||||
type NotFoundError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// 500
|
||||
type ServerError struct {
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
type errCodeMapping struct {
|
||||
target error
|
||||
code int
|
||||
}
|
||||
|
||||
// ErrToCode just returns a mapping to pair a target error with a code
|
||||
func ErrToCode(target error, code int) errCodeMapping {
|
||||
return errCodeMapping{
|
||||
target: target,
|
||||
code: code,
|
||||
}
|
||||
}
|
||||
|
||||
// MapErrorCode checks if err matches any “target” in the mappings,
|
||||
// returning the first matching code. If none match, returns 500.
|
||||
func MapErrorCode(err error, mappings ...errCodeMapping) int {
|
||||
if err == nil {
|
||||
return http.StatusOK
|
||||
}
|
||||
for _, m := range mappings {
|
||||
if errors.Is(err, m.target) {
|
||||
return m.code
|
||||
}
|
||||
}
|
||||
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// CodeToAPIError returns an instance of the correct struct
|
||||
// for the given HTTP code, embedding the supplied message.
|
||||
func CodeToAPIError(code int, message string) any {
|
||||
switch code {
|
||||
case http.StatusNotFound:
|
||||
return NotFoundError{
|
||||
Error: struct {
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
|
||||
case http.StatusUnauthorized:
|
||||
return UnauthorizedError{
|
||||
Error: struct {
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
|
||||
case http.StatusBadRequest:
|
||||
return ValidationError{
|
||||
Error: struct {
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
|
||||
default:
|
||||
return ServerError{
|
||||
Error: struct {
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
90
core/api/util/util.go
Normal file
90
core/api/util/util.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pb/service"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
"github.com/anyproto/anytype-heart/util/pbtypes"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrFailedSearchType = errors.New("failed to search for type")
|
||||
ErrorTypeNotFound = errors.New("type not found")
|
||||
)
|
||||
|
||||
// GetIconFromEmojiOrImage returns the icon to use for the object, which can be either an emoji or an image url
|
||||
func GetIconFromEmojiOrImage(accountInfo *model.AccountInfo, iconEmoji string, iconImage string) string {
|
||||
if iconEmoji != "" {
|
||||
return iconEmoji
|
||||
}
|
||||
|
||||
if iconImage != "" {
|
||||
return fmt.Sprintf("%s/image/%s", accountInfo.GatewayUrl, iconImage)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ResolveTypeToName resolves the type ID to the name of the type, e.g. "ot-page" to "Page" or "bafyreigyb6l5szohs32ts26ku2j42yd65e6hqy2u3gtzgdwqv6hzftsetu" to "Custom Type"
|
||||
func ResolveTypeToName(mw service.ClientCommandsServer, spaceId string, typeId string) (typeName string, err error) {
|
||||
// Can't look up preinstalled types based on relation key, therefore need to use unique key
|
||||
relKey := bundle.RelationKeyId.String()
|
||||
if strings.HasPrefix(typeId, "ot-") {
|
||||
relKey = bundle.RelationKeyUniqueKey.String()
|
||||
}
|
||||
|
||||
// Call ObjectSearch for object of specified type and return the name
|
||||
resp := mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
Operator: model.BlockContentDataviewFilter_No,
|
||||
RelationKey: relKey,
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(typeId),
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyName.String()},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return "", ErrFailedSearchType
|
||||
}
|
||||
|
||||
if len(resp.Records) == 0 {
|
||||
return "", ErrorTypeNotFound
|
||||
}
|
||||
|
||||
return resp.Records[0].Fields[bundle.RelationKeyName.String()].GetStringValue(), nil
|
||||
}
|
||||
|
||||
func ResolveUniqueKeyToTypeId(mw service.ClientCommandsServer, spaceId string, uniqueKey string) (typeId string, err error) {
|
||||
// Call ObjectSearch for type with unique key and return the type's ID
|
||||
resp := mw.ObjectSearch(context.Background(), &pb.RpcObjectSearchRequest{
|
||||
SpaceId: spaceId,
|
||||
Filters: []*model.BlockContentDataviewFilter{
|
||||
{
|
||||
RelationKey: bundle.RelationKeyUniqueKey.String(),
|
||||
Condition: model.BlockContentDataviewFilter_Equal,
|
||||
Value: pbtypes.String(uniqueKey),
|
||||
},
|
||||
},
|
||||
Keys: []string{bundle.RelationKeyId.String()},
|
||||
})
|
||||
|
||||
if resp.Error.Code != pb.RpcObjectSearchResponseError_NULL {
|
||||
return "", ErrFailedSearchType
|
||||
}
|
||||
|
||||
if len(resp.Records) == 0 {
|
||||
return "", ErrorTypeNotFound
|
||||
}
|
||||
|
||||
return resp.Records[0].Fields[bundle.RelationKeyId.String()].GetStringValue(), nil
|
||||
}
|
|
@ -1,9 +1,13 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/anyproto/any-sync/app"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/anytype/config"
|
||||
"github.com/anyproto/anytype-heart/core/api"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
)
|
||||
|
||||
|
@ -24,3 +28,14 @@ func (s *Service) AccountConfigUpdate(req *pb.RpcAccountConfigUpdateRequest) err
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) AccountChangeJsonApiAddr(ctx context.Context, addr string) error {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
if s.app == nil {
|
||||
return ErrApplicationIsNotRunning
|
||||
}
|
||||
apiService := app.MustComponent[api.Service](s.app)
|
||||
return apiService.ReassignAddress(ctx, addr)
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@ import (
|
|||
"github.com/anyproto/anytype-heart/core/anytype/account"
|
||||
"github.com/anyproto/anytype-heart/core/anytype/config"
|
||||
"github.com/anyproto/anytype-heart/core/block"
|
||||
"github.com/anyproto/anytype-heart/core/domain"
|
||||
"github.com/anyproto/anytype-heart/core/block/detailservice"
|
||||
"github.com/anyproto/anytype-heart/core/domain"
|
||||
"github.com/anyproto/anytype-heart/core/domain/objectorigin"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
|
@ -57,6 +57,9 @@ func (s *Service) AccountCreate(ctx context.Context, req *pb.RpcAccountCreateReq
|
|||
cfg.NetworkMode = req.NetworkMode
|
||||
cfg.NetworkCustomConfigFilePath = req.NetworkCustomConfigFilePath
|
||||
}
|
||||
if req.JsonApiListenAddr != "" {
|
||||
cfg.JsonApiListenAddr = req.JsonApiListenAddr
|
||||
}
|
||||
comps := []app.Component{
|
||||
cfg,
|
||||
anytype.BootstrapWallet(s.rootPath, derivationResult),
|
||||
|
|
|
@ -31,15 +31,23 @@ var (
|
|||
ErrNoMnemonicProvided = errors.New("no mnemonic provided")
|
||||
ErrIncompatibleVersion = errors.New("can't fetch account's data because remote nodes have incompatible protocol version. Please update anytype to the latest version")
|
||||
|
||||
ErrAnotherProcessIsRunning = errors.New("another anytype process is running")
|
||||
ErrFailedToFindAccountInfo = errors.New("failed to find account info")
|
||||
ErrAccountIsDeleted = errors.New("account is deleted")
|
||||
ErrAnotherProcessIsRunning = errors.New("another anytype process is running")
|
||||
ErrFailedToFindAccountInfo = errors.New("failed to find account info")
|
||||
ErrAccountIsDeleted = errors.New("account is deleted")
|
||||
ErrAccountStoreIsNotMigrated = errors.New("account store is not migrated")
|
||||
)
|
||||
|
||||
func (s *Service) AccountSelect(ctx context.Context, req *pb.RpcAccountSelectRequest) (*model.Account, error) {
|
||||
if req.Id == "" {
|
||||
return nil, ErrEmptyAccountID
|
||||
}
|
||||
curMigration := s.migrationManager.getOrCreateMigration(req.RootPath, req.Id)
|
||||
if !curMigration.successful() {
|
||||
return nil, ErrAccountStoreIsNotMigrated
|
||||
}
|
||||
if s.migrationManager.isRunning() {
|
||||
return nil, ErrMigrationRunning
|
||||
}
|
||||
|
||||
if runtime.GOOS != "android" && runtime.GOOS != "ios" {
|
||||
s.traceRecorder.start()
|
||||
|
@ -74,10 +82,10 @@ func (s *Service) AccountSelect(ctx context.Context, req *pb.RpcAccountSelectReq
|
|||
}
|
||||
metrics.Service.SetWorkingDir(req.RootPath, req.Id)
|
||||
|
||||
return s.start(ctx, req.Id, req.RootPath, req.DisableLocalNetworkSync, req.PreferYamuxTransport, req.NetworkMode, req.NetworkCustomConfigFilePath)
|
||||
return s.start(ctx, req.Id, req.RootPath, req.DisableLocalNetworkSync, req.JsonApiListenAddr, req.PreferYamuxTransport, req.NetworkMode, req.NetworkCustomConfigFilePath)
|
||||
}
|
||||
|
||||
func (s *Service) start(ctx context.Context, id string, rootPath string, disableLocalNetworkSync bool, preferYamux bool, networkMode pb.RpcAccountNetworkMode, networkConfigFilePath string) (*model.Account, error) {
|
||||
func (s *Service) start(ctx context.Context, id string, rootPath string, disableLocalNetworkSync bool, jsonApiListenAddr string, preferYamux bool, networkMode pb.RpcAccountNetworkMode, networkConfigFilePath string) (*model.Account, error) {
|
||||
ctx, task := trace2.NewTask(ctx, "application.start")
|
||||
defer task.End()
|
||||
|
||||
|
@ -108,6 +116,10 @@ func (s *Service) start(ctx context.Context, id string, rootPath string, disable
|
|||
if disableLocalNetworkSync {
|
||||
cfg.DontStartLocalNetworkSyncAutomatically = true
|
||||
}
|
||||
|
||||
if jsonApiListenAddr != "" {
|
||||
cfg.JsonApiListenAddr = jsonApiListenAddr
|
||||
}
|
||||
if preferYamux {
|
||||
cfg.PeferYamuxTransport = true
|
||||
}
|
||||
|
|
|
@ -89,7 +89,7 @@ func (s *Service) AccountChangeNetworkConfigAndRestart(ctx context.Context, req
|
|||
return ErrFailedToStopApplication
|
||||
}
|
||||
|
||||
_, err = s.start(ctx, accountId, rootPath, conf.DontStartLocalNetworkSyncAutomatically, conf.PeferYamuxTransport, req.NetworkMode, req.NetworkCustomConfigFilePath)
|
||||
_, err = s.start(ctx, accountId, rootPath, conf.DontStartLocalNetworkSyncAutomatically, conf.JsonApiListenAddr, conf.PeferYamuxTransport, req.NetworkMode, req.NetworkCustomConfigFilePath)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
218
core/application/account_store_migrate.go
Normal file
218
core/application/account_store_migrate.go
Normal file
|
@ -0,0 +1,218 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/anyproto/any-sync/app"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/anytype"
|
||||
"github.com/anyproto/anytype-heart/core/anytype/config"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/core"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAccountNotFound = errors.New("account not found")
|
||||
ErrMigrationRunning = errors.New("migration is running")
|
||||
)
|
||||
|
||||
func (s *Service) AccountMigrate(ctx context.Context, req *pb.RpcAccountMigrateRequest) error {
|
||||
if s.rootPath == "" {
|
||||
s.rootPath = req.RootPath
|
||||
}
|
||||
return s.migrationManager.getOrCreateMigration(req.RootPath, req.Id).wait()
|
||||
}
|
||||
|
||||
func (s *Service) AccountMigrateCancel(ctx context.Context, req *pb.RpcAccountMigrateCancelRequest) error {
|
||||
m := s.migrationManager.getMigration(req.Id)
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
m.cancelMigration()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) migrate(ctx context.Context, id string) error {
|
||||
res, err := core.WalletAccountAt(s.mnemonic, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(s.rootPath, id)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return ErrAccountNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
cfg := anytype.BootstrapConfig(false, os.Getenv("ANYTYPE_STAGING") == "1")
|
||||
cfg.PeferYamuxTransport = true
|
||||
cfg.DisableNetworkIdCheck = true
|
||||
comps := []app.Component{
|
||||
cfg,
|
||||
anytype.BootstrapWallet(s.rootPath, res),
|
||||
s.eventSender,
|
||||
}
|
||||
a := &app.App{}
|
||||
anytype.BootstrapMigration(a, comps...)
|
||||
err = a.Start(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.Close(ctx)
|
||||
}
|
||||
|
||||
type migration struct {
|
||||
mx sync.Mutex
|
||||
isStarted bool
|
||||
isFinished bool
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
manager *migrationManager
|
||||
err error
|
||||
id string
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func newMigration(m *migrationManager, id string) *migration {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &migration{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
done: make(chan struct{}),
|
||||
id: id,
|
||||
manager: m,
|
||||
}
|
||||
}
|
||||
|
||||
func newSuccessfulMigration(manager *migrationManager, id string) *migration {
|
||||
m := newMigration(manager, id)
|
||||
m.setFinished(nil, false)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *migration) setFinished(err error, notify bool) {
|
||||
m.mx.Lock()
|
||||
defer m.mx.Unlock()
|
||||
m.isFinished = true
|
||||
m.err = err
|
||||
close(m.done)
|
||||
if notify {
|
||||
m.manager.setMigrationRunning(m.id, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *migration) cancelMigration() {
|
||||
m.cancel()
|
||||
_ = m.wait()
|
||||
}
|
||||
|
||||
func (m *migration) wait() error {
|
||||
m.mx.Lock()
|
||||
if !m.manager.setMigrationRunning(m.id, true) {
|
||||
m.mx.Unlock()
|
||||
return ErrMigrationRunning
|
||||
}
|
||||
if !m.isStarted {
|
||||
m.isStarted = true
|
||||
} else {
|
||||
m.mx.Unlock()
|
||||
<-m.done
|
||||
return m.err
|
||||
}
|
||||
m.mx.Unlock()
|
||||
err := m.manager.service.migrate(m.ctx, m.id)
|
||||
if err != nil {
|
||||
m.setFinished(err, true)
|
||||
return err
|
||||
}
|
||||
m.setFinished(nil, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *migration) successful() bool {
|
||||
m.mx.Lock()
|
||||
defer m.mx.Unlock()
|
||||
return m.isFinished && m.err == nil
|
||||
}
|
||||
|
||||
func (m *migration) finished() bool {
|
||||
m.mx.Lock()
|
||||
defer m.mx.Unlock()
|
||||
return m.isFinished
|
||||
}
|
||||
|
||||
type migrationManager struct {
|
||||
migrations map[string]*migration
|
||||
service *Service
|
||||
runningMigration string
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func newMigrationManager(s *Service) *migrationManager {
|
||||
return &migrationManager{
|
||||
service: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *migrationManager) setMigrationRunning(id string, isRunning bool) bool {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
if (m.runningMigration != "" && m.runningMigration != id) && isRunning {
|
||||
return false
|
||||
}
|
||||
if m.runningMigration == "" && !isRunning {
|
||||
panic("migration is not running")
|
||||
}
|
||||
if isRunning {
|
||||
m.runningMigration = id
|
||||
} else {
|
||||
m.runningMigration = ""
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *migrationManager) isRunning() bool {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return m.runningMigration != ""
|
||||
}
|
||||
|
||||
func (m *migrationManager) getOrCreateMigration(rootPath, id string) *migration {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
if m.migrations == nil {
|
||||
m.migrations = make(map[string]*migration)
|
||||
}
|
||||
if m.migrations[id] == nil {
|
||||
sqlitePath := filepath.Join(rootPath, id, config.SpaceStoreSqlitePath)
|
||||
baderPath := filepath.Join(rootPath, id, config.SpaceStoreBadgerPath)
|
||||
if anyPathExists([]string{sqlitePath, baderPath}) {
|
||||
m.migrations[id] = newMigration(m, id)
|
||||
} else {
|
||||
m.migrations[id] = newSuccessfulMigration(m, id)
|
||||
}
|
||||
}
|
||||
if m.migrations[id].finished() && !m.migrations[id].successful() {
|
||||
// resetting migration
|
||||
m.migrations[id] = newMigration(m, id)
|
||||
}
|
||||
return m.migrations[id]
|
||||
}
|
||||
|
||||
func (m *migrationManager) getMigration(id string) *migration {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return m.migrations[id]
|
||||
}
|
||||
|
||||
func anyPathExists(paths []string) bool {
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -30,16 +30,20 @@ type Service struct {
|
|||
eventSender event.Sender
|
||||
sessions session.Service
|
||||
traceRecorder *traceRecorder
|
||||
migrationManager *migrationManager
|
||||
|
||||
appAccountStartInProcessCancel context.CancelFunc
|
||||
appAccountStartInProcessCancelMutex sync.Mutex
|
||||
}
|
||||
|
||||
func New() *Service {
|
||||
return &Service{
|
||||
s := &Service{
|
||||
sessions: session.New(),
|
||||
traceRecorder: &traceRecorder{},
|
||||
}
|
||||
m := newMigrationManager(s)
|
||||
s.migrationManager = m
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Service) GetApp() *app.App {
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/anyproto/anytype-heart/core/session"
|
||||
walletComp "github.com/anyproto/anytype-heart/core/wallet"
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
)
|
||||
|
||||
func (s *Service) CreateSession(req *pb.RpcWalletCreateSessionRequest) (token string, accountId string, err error) {
|
||||
|
@ -31,7 +32,8 @@ func (s *Service) CreateSession(req *pb.RpcWalletCreateSessionRequest) (token st
|
|||
return "", "", err
|
||||
}
|
||||
log.Infof("appLink auth %s", appLink.AppName)
|
||||
token, err := s.sessions.StartSession(s.sessionSigningKey)
|
||||
|
||||
token, err := s.sessions.StartSession(s.sessionSigningKey, model.AccountAuthLocalApiScope(appLink.Scope)) // nolint:gosec
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
@ -46,7 +48,7 @@ func (s *Service) CreateSession(req *pb.RpcWalletCreateSessionRequest) (token st
|
|||
if s.mnemonic != mnemonic {
|
||||
return "", "", errors.Join(ErrBadInput, fmt.Errorf("incorrect mnemonic"))
|
||||
}
|
||||
token, err = s.sessions.StartSession(s.sessionSigningKey)
|
||||
token, err = s.sessions.StartSession(s.sessionSigningKey, model.AccountAuth_Full)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
@ -61,16 +63,16 @@ func (s *Service) CloseSession(req *pb.RpcWalletCloseSessionRequest) error {
|
|||
return s.sessions.CloseSession(req.Token)
|
||||
}
|
||||
|
||||
func (s *Service) ValidateSessionToken(token string) error {
|
||||
func (s *Service) ValidateSessionToken(token string) (model.AccountAuthLocalApiScope, error) {
|
||||
return s.sessions.ValidateToken(s.sessionSigningKey, token)
|
||||
}
|
||||
|
||||
func (s *Service) LinkLocalStartNewChallenge(clientInfo *pb.EventAccountLinkChallengeClientInfo) (id string, err error) {
|
||||
func (s *Service) LinkLocalStartNewChallenge(scope model.AccountAuthLocalApiScope, clientInfo *pb.EventAccountLinkChallengeClientInfo) (id string, err error) {
|
||||
if s.app == nil {
|
||||
return "", ErrApplicationIsNotRunning
|
||||
}
|
||||
|
||||
id, value, err := s.sessions.StartNewChallenge(clientInfo)
|
||||
id, value, err := s.sessions.StartNewChallenge(scope, clientInfo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -78,6 +80,7 @@ func (s *Service) LinkLocalStartNewChallenge(clientInfo *pb.EventAccountLinkChal
|
|||
AccountLinkChallenge: &pb.EventAccountLinkChallenge{
|
||||
Challenge: value,
|
||||
ClientInfo: clientInfo,
|
||||
Scope: scope,
|
||||
},
|
||||
}))
|
||||
return id, nil
|
||||
|
@ -87,7 +90,7 @@ func (s *Service) LinkLocalSolveChallenge(req *pb.RpcAccountLocalLinkSolveChalle
|
|||
if s.app == nil {
|
||||
return "", "", ErrApplicationIsNotRunning
|
||||
}
|
||||
clientInfo, token, err := s.sessions.SolveChallenge(req.ChallengeId, req.Answer, s.sessionSigningKey)
|
||||
clientInfo, token, scope, err := s.sessions.SolveChallenge(req.ChallengeId, req.Answer, s.sessionSigningKey)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
@ -96,7 +99,13 @@ func (s *Service) LinkLocalSolveChallenge(req *pb.RpcAccountLocalLinkSolveChalle
|
|||
AppName: clientInfo.ProcessName,
|
||||
AppPath: clientInfo.ProcessPath,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
Scope: int(scope),
|
||||
})
|
||||
|
||||
s.eventSender.Broadcast(event.NewEventSingleMessage("", &pb.EventMessageValueOfAccountLinkChallengeHide{
|
||||
AccountLinkChallengeHide: &pb.EventAccountLinkChallengeHide{
|
||||
Challenge: req.Answer,
|
||||
},
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
|
31
core/auth.go
31
core/auth.go
|
@ -6,6 +6,7 @@ package core
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"github.com/gogo/protobuf/protoc-gen-gogo/descriptor"
|
||||
|
@ -15,8 +16,24 @@ import (
|
|||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/anyproto/anytype-heart/pb"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
|
||||
)
|
||||
|
||||
var limitedScopeMethods = map[string]struct{}{
|
||||
"ObjectSearch": {},
|
||||
"ObjectShow": {},
|
||||
"ObjectCreate": {},
|
||||
"ObjectCreateFromUrl": {},
|
||||
"BlockPreview": {},
|
||||
"BlockPaste": {},
|
||||
"BroadcastPayloadEvent": {},
|
||||
"AccountSelect": {},
|
||||
"ListenSessionEvents": {},
|
||||
"ObjectSearchSubscribe": {},
|
||||
"ObjectCreateRelationOption": {},
|
||||
// need to replace with other method to get info
|
||||
}
|
||||
|
||||
func (mw *Middleware) Authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
|
||||
_, d := descriptor.ForMessage(req.(descriptor.Message))
|
||||
noAuth := proto.GetBoolExtension(d.GetOptions(), pb.E_NoAuth, false)
|
||||
|
@ -35,11 +52,21 @@ func (mw *Middleware) Authorize(ctx context.Context, req interface{}, info *grpc
|
|||
}
|
||||
tok := v[0]
|
||||
|
||||
err = mw.applicationService.ValidateSessionToken(tok)
|
||||
var scope model.AccountAuthLocalApiScope
|
||||
scope, err = mw.applicationService.ValidateSessionToken(tok)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Unauthenticated, err.Error())
|
||||
}
|
||||
|
||||
switch scope {
|
||||
case model.AccountAuth_Full:
|
||||
case model.AccountAuth_Limited:
|
||||
methodTrimmed := strings.TrimPrefix(info.FullMethod, "/anytype.ClientCommands/")
|
||||
if _, ok := limitedScopeMethods[methodTrimmed]; !ok {
|
||||
return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("method %s not allowed for %s", methodTrimmed, scope.String()))
|
||||
}
|
||||
default:
|
||||
return nil, status.Error(codes.PermissionDenied, fmt.Sprintf("method %s not allowed for %s scope", info.FullMethod, scope.String()))
|
||||
}
|
||||
resp, err = handler(ctx, req)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/anyproto/any-sync/app/ocache"
|
||||
"github.com/cheggaaa/mb"
|
||||
"github.com/cheggaaa/mb/v3"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
|
@ -56,10 +56,12 @@ type watcher struct {
|
|||
resolver idresolver.Resolver
|
||||
spaceService space.Service
|
||||
|
||||
infoBatch *mb.MB
|
||||
infoBatch *mb.MB[spaceindex.LinksUpdateInfo]
|
||||
lock sync.Mutex
|
||||
accumulatedBacklinks map[string]*backLinksUpdate
|
||||
aggregationInterval time.Duration
|
||||
cancelCtx context.CancelFunc
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func New() UpdateWatcher {
|
||||
|
@ -75,22 +77,26 @@ func (w *watcher) Init(a *app.App) error {
|
|||
w.store = app.MustComponent[objectstore.ObjectStore](a)
|
||||
w.resolver = app.MustComponent[idresolver.Resolver](a)
|
||||
w.spaceService = app.MustComponent[space.Service](a)
|
||||
w.infoBatch = mb.New(0)
|
||||
w.infoBatch = mb.New[spaceindex.LinksUpdateInfo](0)
|
||||
w.accumulatedBacklinks = make(map[string]*backLinksUpdate)
|
||||
w.aggregationInterval = defaultAggregationInterval
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *watcher) Close(context.Context) error {
|
||||
if w.cancelCtx != nil {
|
||||
w.cancelCtx()
|
||||
}
|
||||
if err := w.infoBatch.Close(); err != nil {
|
||||
log.Errorf("failed to close message batch: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *watcher) Run(context.Context) error {
|
||||
func (w *watcher) Run(ctx context.Context) error {
|
||||
w.ctx, w.cancelCtx = context.WithCancel(context.Background())
|
||||
w.updater.SubscribeLinksUpdate(func(info spaceindex.LinksUpdateInfo) {
|
||||
if err := w.infoBatch.Add(info); err != nil {
|
||||
if err := w.infoBatch.Add(w.ctx, info); err != nil {
|
||||
log.With("objectId", info.LinksFromId).Errorf("failed to add backlinks update info to message batch: %v", err)
|
||||
}
|
||||
})
|
||||
|
@ -165,17 +171,16 @@ func (w *watcher) backlinksUpdateHandler() {
|
|||
}()
|
||||
|
||||
for {
|
||||
msgs := w.infoBatch.Wait()
|
||||
msgs, err := w.infoBatch.Wait(w.ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(msgs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
w.lock.Lock()
|
||||
for _, msg := range msgs {
|
||||
info, ok := msg.(spaceindex.LinksUpdateInfo)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, info := range msgs {
|
||||
info = cleanSelfLinks(info)
|
||||
applyUpdates(w.accumulatedBacklinks, info)
|
||||
}
|
||||
|
@ -210,7 +215,7 @@ func (w *watcher) updateBackLinksInObject(id string, backlinksUpdate *backLinksU
|
|||
log.With("objectId", id).Errorf("failed to resolve space id for object: %v", err)
|
||||
return
|
||||
}
|
||||
spc, err := w.spaceService.Get(context.Background(), spaceId)
|
||||
spc, err := w.spaceService.Get(w.ctx, spaceId)
|
||||
if err != nil {
|
||||
log.With("objectId", id, "spaceId", spaceId).Errorf("failed to get space: %v", err)
|
||||
return
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/app/ocache"
|
||||
"github.com/cheggaaa/mb"
|
||||
"github.com/cheggaaa/mb/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -56,7 +56,7 @@ func newFixture(t *testing.T, aggregationInterval time.Duration) *fixture {
|
|||
spaceService: spaceSvc,
|
||||
|
||||
aggregationInterval: aggregationInterval,
|
||||
infoBatch: mb.New(0),
|
||||
infoBatch: mb.New[spaceindex.LinksUpdateInfo](0),
|
||||
accumulatedBacklinks: make(map[string]*backLinksUpdate),
|
||||
}
|
||||
|
||||
|
|
5
core/block/cache/cache.go
vendored
5
core/block/cache/cache.go
vendored
|
@ -22,6 +22,11 @@ type ObjectGetter interface {
|
|||
GetObjectByFullID(ctx context.Context, id domain.FullID) (sb smartblock.SmartBlock, err error)
|
||||
}
|
||||
|
||||
type CachedObjectGetter interface {
|
||||
ObjectGetter
|
||||
TryRemoveFromCache(ctx context.Context, objectId string) (res bool, err error)
|
||||
}
|
||||
|
||||
func Do[t any](p ObjectGetter, objectID string, apply func(sb t) error) error {
|
||||
ctx := context.Background()
|
||||
sb, err := p.GetObject(ctx, objectID)
|
||||
|
|
214
core/block/cache/mock_cache/mock_CachedObjectGetter.go
vendored
Normal file
214
core/block/cache/mock_cache/mock_CachedObjectGetter.go
vendored
Normal file
|
@ -0,0 +1,214 @@
|
|||
// Code generated by mockery. DO NOT EDIT.
|
||||
|
||||
package mock_cache
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
domain "github.com/anyproto/anytype-heart/core/domain"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
smartblock "github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
)
|
||||
|
||||
// MockCachedObjectGetter is an autogenerated mock type for the CachedObjectGetter type
|
||||
type MockCachedObjectGetter struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockCachedObjectGetter_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockCachedObjectGetter) EXPECT() *MockCachedObjectGetter_Expecter {
|
||||
return &MockCachedObjectGetter_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// GetObject provides a mock function with given fields: ctx, objectID
|
||||
func (_m *MockCachedObjectGetter) GetObject(ctx context.Context, objectID string) (smartblock.SmartBlock, error) {
|
||||
ret := _m.Called(ctx, objectID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetObject")
|
||||
}
|
||||
|
||||
var r0 smartblock.SmartBlock
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (smartblock.SmartBlock, error)); ok {
|
||||
return rf(ctx, objectID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) smartblock.SmartBlock); ok {
|
||||
r0 = rf(ctx, objectID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(smartblock.SmartBlock)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, objectID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockCachedObjectGetter_GetObject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetObject'
|
||||
type MockCachedObjectGetter_GetObject_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetObject is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - objectID string
|
||||
func (_e *MockCachedObjectGetter_Expecter) GetObject(ctx interface{}, objectID interface{}) *MockCachedObjectGetter_GetObject_Call {
|
||||
return &MockCachedObjectGetter_GetObject_Call{Call: _e.mock.On("GetObject", ctx, objectID)}
|
||||
}
|
||||
|
||||
func (_c *MockCachedObjectGetter_GetObject_Call) Run(run func(ctx context.Context, objectID string)) *MockCachedObjectGetter_GetObject_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCachedObjectGetter_GetObject_Call) Return(sb smartblock.SmartBlock, err error) *MockCachedObjectGetter_GetObject_Call {
|
||||
_c.Call.Return(sb, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCachedObjectGetter_GetObject_Call) RunAndReturn(run func(context.Context, string) (smartblock.SmartBlock, error)) *MockCachedObjectGetter_GetObject_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetObjectByFullID provides a mock function with given fields: ctx, id
|
||||
func (_m *MockCachedObjectGetter) GetObjectByFullID(ctx context.Context, id domain.FullID) (smartblock.SmartBlock, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetObjectByFullID")
|
||||
}
|
||||
|
||||
var r0 smartblock.SmartBlock
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.FullID) (smartblock.SmartBlock, error)); ok {
|
||||
return rf(ctx, id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, domain.FullID) smartblock.SmartBlock); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(smartblock.SmartBlock)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, domain.FullID) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockCachedObjectGetter_GetObjectByFullID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetObjectByFullID'
|
||||
type MockCachedObjectGetter_GetObjectByFullID_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetObjectByFullID is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - id domain.FullID
|
||||
func (_e *MockCachedObjectGetter_Expecter) GetObjectByFullID(ctx interface{}, id interface{}) *MockCachedObjectGetter_GetObjectByFullID_Call {
|
||||
return &MockCachedObjectGetter_GetObjectByFullID_Call{Call: _e.mock.On("GetObjectByFullID", ctx, id)}
|
||||
}
|
||||
|
||||
func (_c *MockCachedObjectGetter_GetObjectByFullID_Call) Run(run func(ctx context.Context, id domain.FullID)) *MockCachedObjectGetter_GetObjectByFullID_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(domain.FullID))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCachedObjectGetter_GetObjectByFullID_Call) Return(sb smartblock.SmartBlock, err error) *MockCachedObjectGetter_GetObjectByFullID_Call {
|
||||
_c.Call.Return(sb, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCachedObjectGetter_GetObjectByFullID_Call) RunAndReturn(run func(context.Context, domain.FullID) (smartblock.SmartBlock, error)) *MockCachedObjectGetter_GetObjectByFullID_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// TryRemoveFromCache provides a mock function with given fields: ctx, objectId
|
||||
func (_m *MockCachedObjectGetter) TryRemoveFromCache(ctx context.Context, objectId string) (bool, error) {
|
||||
ret := _m.Called(ctx, objectId)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for TryRemoveFromCache")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok {
|
||||
return rf(ctx, objectId)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok {
|
||||
r0 = rf(ctx, objectId)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, objectId)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockCachedObjectGetter_TryRemoveFromCache_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TryRemoveFromCache'
|
||||
type MockCachedObjectGetter_TryRemoveFromCache_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// TryRemoveFromCache is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - objectId string
|
||||
func (_e *MockCachedObjectGetter_Expecter) TryRemoveFromCache(ctx interface{}, objectId interface{}) *MockCachedObjectGetter_TryRemoveFromCache_Call {
|
||||
return &MockCachedObjectGetter_TryRemoveFromCache_Call{Call: _e.mock.On("TryRemoveFromCache", ctx, objectId)}
|
||||
}
|
||||
|
||||
func (_c *MockCachedObjectGetter_TryRemoveFromCache_Call) Run(run func(ctx context.Context, objectId string)) *MockCachedObjectGetter_TryRemoveFromCache_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCachedObjectGetter_TryRemoveFromCache_Call) Return(res bool, err error) *MockCachedObjectGetter_TryRemoveFromCache_Call {
|
||||
_c.Call.Return(res, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCachedObjectGetter_TryRemoveFromCache_Call) RunAndReturn(run func(context.Context, string) (bool, error)) *MockCachedObjectGetter_TryRemoveFromCache_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockCachedObjectGetter creates a new instance of MockCachedObjectGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockCachedObjectGetter(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockCachedObjectGetter {
|
||||
mock := &MockCachedObjectGetter{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package block
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -22,6 +23,7 @@ import (
|
|||
func (s *Service) DebugRouter(r chi.Router) {
|
||||
r.Get("/objects", debug.JSONHandler(s.debugListObjects))
|
||||
r.Get("/tree/{id}", debug.JSONHandler(s.debugTree))
|
||||
r.Get("/tree_in_space/{spaceId}/{id}", debug.JSONHandler(s.debugTreeInSpace))
|
||||
r.Get("/objects_per_space/{spaceId}", debug.JSONHandler(s.debugListObjectsPerSpace))
|
||||
r.Get("/objects/{id}", debug.JSONHandler(s.debugGetObject))
|
||||
}
|
||||
|
@ -122,6 +124,44 @@ func (s *Service) debugTree(req *http.Request) (debugTree, error) {
|
|||
return result, err
|
||||
}
|
||||
|
||||
// TODO Refactor
|
||||
func (s *Service) debugTreeInSpace(req *http.Request) (debugTree, error) {
|
||||
spaceId := chi.URLParam(req, "spaceId")
|
||||
id := chi.URLParam(req, "id")
|
||||
|
||||
result := debugTree{
|
||||
Id: id,
|
||||
}
|
||||
|
||||
spc, err := s.spaceService.Get(context.Background(), spaceId)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("get space: %w", err)
|
||||
}
|
||||
|
||||
err = spc.Do(id, func(sb smartblock.SmartBlock) error {
|
||||
ot := sb.Tree()
|
||||
return ot.IterateRoot(source.UnmarshalChange, func(change *objecttree.Change) bool {
|
||||
change.Next = nil
|
||||
raw, err := json.Marshal(change)
|
||||
if err != nil {
|
||||
log.Error("debug tree: marshal change", zap.Error(err))
|
||||
return false
|
||||
}
|
||||
ts := time.Unix(change.Timestamp, 0)
|
||||
ch := debugChange{
|
||||
Change: raw,
|
||||
Timestamp: ts.Format(time.RFC3339),
|
||||
}
|
||||
if change.Identity != nil {
|
||||
ch.Identity = change.Identity.Account()
|
||||
}
|
||||
result.Changes = append(result.Changes, ch)
|
||||
return true
|
||||
})
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *Service) getDebugObject(id string) (debugObject, error) {
|
||||
var obj debugObject
|
||||
err := cache.Do(s, id, func(sb smartblock.SmartBlock) error {
|
||||
|
|
|
@ -417,54 +417,6 @@ func (_c *MockService_SetDetails_Call) RunAndReturn(run func(session.Context, st
|
|||
return _c
|
||||
}
|
||||
|
||||
// SetDetailsAndUpdateLastUsed provides a mock function with given fields: ctx, objectId, details
|
||||
func (_m *MockService) SetDetailsAndUpdateLastUsed(ctx session.Context, objectId string, details []domain.Detail) error {
|
||||
ret := _m.Called(ctx, objectId, details)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for SetDetailsAndUpdateLastUsed")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(session.Context, string, []domain.Detail) error); ok {
|
||||
r0 = rf(ctx, objectId, details)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockService_SetDetailsAndUpdateLastUsed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDetailsAndUpdateLastUsed'
|
||||
type MockService_SetDetailsAndUpdateLastUsed_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// SetDetailsAndUpdateLastUsed is a helper method to define mock.On call
|
||||
// - ctx session.Context
|
||||
// - objectId string
|
||||
// - details []domain.Detail
|
||||
func (_e *MockService_Expecter) SetDetailsAndUpdateLastUsed(ctx interface{}, objectId interface{}, details interface{}) *MockService_SetDetailsAndUpdateLastUsed_Call {
|
||||
return &MockService_SetDetailsAndUpdateLastUsed_Call{Call: _e.mock.On("SetDetailsAndUpdateLastUsed", ctx, objectId, details)}
|
||||
}
|
||||
|
||||
func (_c *MockService_SetDetailsAndUpdateLastUsed_Call) Run(run func(ctx session.Context, objectId string, details []domain.Detail)) *MockService_SetDetailsAndUpdateLastUsed_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(session.Context), args[1].(string), args[2].([]domain.Detail))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_SetDetailsAndUpdateLastUsed_Call) Return(_a0 error) *MockService_SetDetailsAndUpdateLastUsed_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockService_SetDetailsAndUpdateLastUsed_Call) RunAndReturn(run func(session.Context, string, []domain.Detail) error) *MockService_SetDetailsAndUpdateLastUsed_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// SetDetailsList provides a mock function with given fields: ctx, objectIds, details
|
||||
func (_m *MockService) SetDetailsList(ctx session.Context, objectIds []string, details []domain.Detail) error {
|
||||
ret := _m.Called(ctx, objectIds, details)
|
||||
|
|
|
@ -31,7 +31,6 @@ type Service interface {
|
|||
app.Component
|
||||
|
||||
SetDetails(ctx session.Context, objectId string, details []domain.Detail) error
|
||||
SetDetailsAndUpdateLastUsed(ctx session.Context, objectId string, details []domain.Detail) error
|
||||
SetDetailsList(ctx session.Context, objectIds []string, details []domain.Detail) error
|
||||
ModifyDetails(ctx session.Context, objectId string, modifier func(current *domain.Details) (*domain.Details, error)) error
|
||||
ModifyDetailsList(req *pb.RpcObjectListModifyDetailValuesRequest) error
|
||||
|
@ -81,23 +80,10 @@ func (s *service) SetDetails(ctx session.Context, objectId string, details []dom
|
|||
})
|
||||
}
|
||||
|
||||
func (s *service) SetDetailsAndUpdateLastUsed(ctx session.Context, objectId string, details []domain.Detail) (err error) {
|
||||
return cache.Do(s.objectGetter, objectId, func(b basic.DetailsSettable) error {
|
||||
return b.SetDetailsAndUpdateLastUsed(ctx, details, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *service) SetDetailsList(ctx session.Context, objectIds []string, details []domain.Detail) (err error) {
|
||||
var (
|
||||
resultError error
|
||||
anySucceed bool
|
||||
)
|
||||
for i, objectId := range objectIds {
|
||||
setDetailsFunc := s.SetDetails
|
||||
if i == 0 {
|
||||
setDetailsFunc = s.SetDetailsAndUpdateLastUsed
|
||||
}
|
||||
err := setDetailsFunc(ctx, objectId, details)
|
||||
func (s *service) SetDetailsList(ctx session.Context, objectIds []string, details []domain.Detail) (resultError error) {
|
||||
var anySucceed bool
|
||||
for _, objectId := range objectIds {
|
||||
err := s.SetDetails(ctx, objectId, details)
|
||||
if err != nil {
|
||||
resultError = errors.Join(resultError, err)
|
||||
} else {
|
||||
|
@ -120,20 +106,10 @@ func (s *service) ModifyDetails(ctx session.Context, objectId string, modifier f
|
|||
})
|
||||
}
|
||||
|
||||
func (s *service) ModifyDetailsAndUpdateLastUsed(ctx session.Context, objectId string, modifier func(current *domain.Details) (*domain.Details, error)) (err error) {
|
||||
return cache.Do(s.objectGetter, objectId, func(du basic.DetailsUpdatable) error {
|
||||
return du.UpdateDetailsAndLastUsed(ctx, modifier)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *service) ModifyDetailsList(req *pb.RpcObjectListModifyDetailValuesRequest) (resultError error) {
|
||||
var anySucceed bool
|
||||
for i, objectId := range req.ObjectIds {
|
||||
modifyDetailsFunc := s.ModifyDetails
|
||||
if i == 0 {
|
||||
modifyDetailsFunc = s.ModifyDetailsAndUpdateLastUsed
|
||||
}
|
||||
err := modifyDetailsFunc(nil, objectId, func(current *domain.Details) (*domain.Details, error) {
|
||||
for _, objectId := range req.ObjectIds {
|
||||
err := s.ModifyDetails(nil, objectId, func(current *domain.Details) (*domain.Details, error) {
|
||||
for _, op := range req.Operations {
|
||||
if !pbtypes.IsNullValue(op.Set) {
|
||||
// Set operation has higher priority than Add and Remove, because it modifies full value
|
||||
|
|
|
@ -79,7 +79,7 @@ func TestService_SetDetailsList(t *testing.T) {
|
|||
{Key: bundle.RelationKeyLinkedProjects, Value: domain.StringList([]string{"important", "urgent"})},
|
||||
}
|
||||
|
||||
t.Run("lastUsed is updated once", func(t *testing.T) {
|
||||
t.Run("no error", func(t *testing.T) {
|
||||
// given
|
||||
fx := newFixture(t)
|
||||
objects := map[string]*smarttest.SmartTest{
|
||||
|
@ -98,16 +98,6 @@ func TestService_SetDetailsList(t *testing.T) {
|
|||
|
||||
// then
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, objects["obj1"].Results.LastUsedUpdates, 3)
|
||||
assert.Equal(t, []string{
|
||||
bundle.RelationKeyAssignee.String(),
|
||||
bundle.RelationKeyDone.String(),
|
||||
bundle.RelationKeyLinkedProjects.String(),
|
||||
}, objects["obj1"].Results.LastUsedUpdates)
|
||||
|
||||
// lastUsed should be updated only during the work under 1st object
|
||||
assert.Len(t, objects["obj2"].Results.LastUsedUpdates, 0)
|
||||
assert.Len(t, objects["obj3"].Results.LastUsedUpdates, 0)
|
||||
|
||||
assert.Equal(t, "Mark Twain", objects["obj1"].NewState().Details().GetString(bundle.RelationKeyAssignee))
|
||||
assert.True(t, objects["obj2"].NewState().Details().GetBool(bundle.RelationKeyDone))
|
||||
|
@ -153,7 +143,7 @@ func TestService_ModifyDetailsList(t *testing.T) {
|
|||
{RelationKey: bundle.RelationKeyDone.String(), Set: domain.Bool(true).ToProto()},
|
||||
}
|
||||
|
||||
t.Run("lastUsed is updated once", func(t *testing.T) {
|
||||
t.Run("no error", func(t *testing.T) {
|
||||
fx := newFixture(t)
|
||||
objects := map[string]*smarttest.SmartTest{
|
||||
"obj1": smarttest.New("obj1"),
|
||||
|
@ -174,11 +164,6 @@ func TestService_ModifyDetailsList(t *testing.T) {
|
|||
|
||||
// then
|
||||
assert.NoError(t, err)
|
||||
require.Len(t, objects["obj1"].Results.LastUsedUpdates, 3)
|
||||
|
||||
// lastUsed should be updated only during the work under 1st object
|
||||
assert.Len(t, objects["obj2"].Results.LastUsedUpdates, 0)
|
||||
assert.Len(t, objects["obj3"].Results.LastUsedUpdates, 0)
|
||||
})
|
||||
|
||||
t.Run("some updates failed", func(t *testing.T) {
|
||||
|
@ -306,7 +291,7 @@ func TestService_SetWorkspaceDashboardId(t *testing.T) {
|
|||
assert.Equal(t, wsObjectId, objectId)
|
||||
ws := &editor.Workspaces{
|
||||
SmartBlock: sb,
|
||||
AllOperations: basic.NewBasic(sb, fx.store.SpaceIndex(spaceId), nil, nil, nil),
|
||||
AllOperations: basic.NewBasic(sb, fx.store.SpaceIndex(spaceId), nil, nil),
|
||||
}
|
||||
return ws, nil
|
||||
})
|
||||
|
@ -329,7 +314,7 @@ func TestService_SetWorkspaceDashboardId(t *testing.T) {
|
|||
assert.Equal(t, wsObjectId, objectId)
|
||||
ws := &editor.Workspaces{
|
||||
SmartBlock: sb,
|
||||
AllOperations: basic.NewBasic(sb, fx.store.SpaceIndex(spaceId), nil, nil, nil),
|
||||
AllOperations: basic.NewBasic(sb, fx.store.SpaceIndex(spaceId), nil, nil),
|
||||
}
|
||||
return ws, nil
|
||||
})
|
||||
|
|
|
@ -18,7 +18,6 @@ import (
|
|||
"github.com/anyproto/anytype-heart/core/block/editor/anystoredebug"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/basic"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/converter"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/state"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/storestate"
|
||||
|
@ -88,23 +87,18 @@ func (a *accountObject) SetDetails(ctx session.Context, details []domain.Detail,
|
|||
return a.bs.SetDetails(ctx, details, showEvent)
|
||||
}
|
||||
|
||||
func (a *accountObject) SetDetailsAndUpdateLastUsed(ctx session.Context, details []domain.Detail, showEvent bool) (err error) {
|
||||
return a.bs.SetDetailsAndUpdateLastUsed(ctx, details, showEvent)
|
||||
}
|
||||
|
||||
func New(
|
||||
sb smartblock.SmartBlock,
|
||||
keys *accountdata.AccountKeys,
|
||||
spaceObjects spaceindex.Store,
|
||||
layoutConverter converter.LayoutConverter,
|
||||
fileObjectService fileobject.Service,
|
||||
lastUsedUpdater lastused.ObjectUsageUpdater,
|
||||
crdtDb anystore.DB,
|
||||
cfg *config.Config) AccountObject {
|
||||
return &accountObject{
|
||||
crdtDb: crdtDb,
|
||||
keys: keys,
|
||||
bs: basic.NewBasic(sb, spaceObjects, layoutConverter, fileObjectService, lastUsedUpdater),
|
||||
bs: basic.NewBasic(sb, spaceObjects, layoutConverter, fileObjectService),
|
||||
SmartBlock: sb,
|
||||
cfg: cfg,
|
||||
relMapper: newRelationsMapper(map[string]KeyType{
|
||||
|
|
|
@ -55,7 +55,7 @@ func newFixture(t *testing.T, isNewAccount bool, prepareDb func(db anystore.DB))
|
|||
indexStore := objectstore.NewStoreFixture(t).SpaceIndex("spaceId")
|
||||
keys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
object := New(sb, keys, indexStore, nil, nil, nil, db, cfg)
|
||||
object := New(sb, keys, indexStore, nil, nil, db, cfg)
|
||||
fx := &fixture{
|
||||
storeFx: objectstore.NewStoreFixture(t),
|
||||
db: db,
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"github.com/samber/lo"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/converter"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/state"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/table"
|
||||
|
@ -64,12 +63,10 @@ type CommonOperations interface {
|
|||
|
||||
type DetailsSettable interface {
|
||||
SetDetails(ctx session.Context, details []domain.Detail, showEvent bool) (err error)
|
||||
SetDetailsAndUpdateLastUsed(ctx session.Context, details []domain.Detail, showEvent bool) (err error)
|
||||
}
|
||||
|
||||
type DetailsUpdatable interface {
|
||||
UpdateDetails(ctx session.Context, update func(current *domain.Details) (*domain.Details, error)) (err error)
|
||||
UpdateDetailsAndLastUsed(ctx session.Context, update func(current *domain.Details) (*domain.Details, error)) (err error)
|
||||
}
|
||||
|
||||
type Restrictionable interface {
|
||||
|
@ -105,14 +102,12 @@ func NewBasic(
|
|||
objectStore spaceindex.Store,
|
||||
layoutConverter converter.LayoutConverter,
|
||||
fileObjectService fileobject.Service,
|
||||
lastUsedUpdater lastused.ObjectUsageUpdater,
|
||||
) AllOperations {
|
||||
return &basic{
|
||||
SmartBlock: sb,
|
||||
objectStore: objectStore,
|
||||
layoutConverter: layoutConverter,
|
||||
fileObjectService: fileObjectService,
|
||||
lastUsedUpdater: lastUsedUpdater,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,7 +117,6 @@ type basic struct {
|
|||
objectStore spaceindex.Store
|
||||
layoutConverter converter.LayoutConverter
|
||||
fileObjectService fileobject.Service
|
||||
lastUsedUpdater lastused.ObjectUsageUpdater
|
||||
}
|
||||
|
||||
func (bs *basic) CreateBlock(s *state.State, req pb.RpcBlockCreateRequest) (id string, err error) {
|
||||
|
|
|
@ -45,7 +45,7 @@ func TestBasic_Create(t *testing.T) {
|
|||
t.Run("generic", func(t *testing.T) {
|
||||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
id, err := b.CreateBlock(st, pb.RpcBlockCreateRequest{
|
||||
Block: &model.Block{Content: &model.BlockContentOfText{Text: &model.BlockContentText{Text: "ll"}}},
|
||||
|
@ -59,7 +59,7 @@ func TestBasic_Create(t *testing.T) {
|
|||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
|
||||
require.NoError(t, smartblock.ObjectApplyTemplate(sb, sb.NewState(), template.WithTitle))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
s := sb.NewState()
|
||||
id, err := b.CreateBlock(s, pb.RpcBlockCreateRequest{
|
||||
TargetId: template.TitleBlockId,
|
||||
|
@ -80,7 +80,7 @@ func TestBasic_Create(t *testing.T) {
|
|||
}
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
|
||||
require.NoError(t, smartblock.ObjectApplyTemplate(sb, sb.NewState(), template.WithTitle))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
_, err := b.CreateBlock(sb.NewState(), pb.RpcBlockCreateRequest{})
|
||||
assert.ErrorIs(t, err, restriction.ErrRestricted)
|
||||
})
|
||||
|
@ -94,7 +94,7 @@ func TestBasic_Duplicate(t *testing.T) {
|
|||
AddBlock(simple.New(&model.Block{Id: "3"}))
|
||||
|
||||
st := sb.NewState()
|
||||
newIds, err := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil).Duplicate(st, st, "", 0, []string{"2"})
|
||||
newIds, err := NewBasic(sb, nil, converter.NewLayoutConverter(), nil).Duplicate(st, st, "", 0, []string{"2"})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = sb.Apply(st)
|
||||
|
@ -172,7 +172,7 @@ func TestBasic_Duplicate(t *testing.T) {
|
|||
ts.SetDetail(bundle.RelationKeySpaceId, domain.String(tc.spaceIds[1]))
|
||||
|
||||
// when
|
||||
newIds, err := NewBasic(source, nil, nil, tc.fos(), nil).Duplicate(ss, ts, "target", model.Block_Inner, []string{"1", "f1"})
|
||||
newIds, err := NewBasic(source, nil, nil, tc.fos()).Duplicate(ss, ts, "target", model.Block_Inner, []string{"1", "f1"})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, target.Apply(ts))
|
||||
|
||||
|
@ -206,7 +206,7 @@ func TestBasic_Unlink(t *testing.T) {
|
|||
AddBlock(simple.New(&model.Block{Id: "2", ChildrenIds: []string{"3"}})).
|
||||
AddBlock(simple.New(&model.Block{Id: "3"}))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
|
||||
err := b.Unlink(nil, "2")
|
||||
require.NoError(t, err)
|
||||
|
@ -220,7 +220,7 @@ func TestBasic_Unlink(t *testing.T) {
|
|||
AddBlock(simple.New(&model.Block{Id: "2", ChildrenIds: []string{"3"}})).
|
||||
AddBlock(simple.New(&model.Block{Id: "3"}))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
|
||||
err := b.Unlink(nil, "2", "3")
|
||||
require.NoError(t, err)
|
||||
|
@ -237,7 +237,7 @@ func TestBasic_Move(t *testing.T) {
|
|||
AddBlock(simple.New(&model.Block{Id: "3"})).
|
||||
AddBlock(simple.New(&model.Block{Id: "4"}))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
|
||||
err := b.Move(st, st, "4", model.Block_Inner, []string{"3"})
|
||||
|
@ -251,7 +251,7 @@ func TestBasic_Move(t *testing.T) {
|
|||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test"}))
|
||||
require.NoError(t, smartblock.ObjectApplyTemplate(sb, sb.NewState(), template.WithTitle))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
s := sb.NewState()
|
||||
id1, err := b.CreateBlock(s, pb.RpcBlockCreateRequest{
|
||||
TargetId: template.HeaderLayoutId,
|
||||
|
@ -300,7 +300,7 @@ func TestBasic_Move(t *testing.T) {
|
|||
},
|
||||
),
|
||||
)
|
||||
basic := NewBasic(testDoc, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
basic := NewBasic(testDoc, nil, converter.NewLayoutConverter(), nil)
|
||||
state := testDoc.NewState()
|
||||
|
||||
// when
|
||||
|
@ -316,7 +316,7 @@ func TestBasic_Move(t *testing.T) {
|
|||
AddBlock(newTextBlock("1", "", nil)).
|
||||
AddBlock(newTextBlock("2", "one", nil))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
err := b.Move(st, st, "1", model.Block_InnerFirst, []string{"2"})
|
||||
require.NoError(t, err)
|
||||
|
@ -336,7 +336,7 @@ func TestBasic_Move(t *testing.T) {
|
|||
AddBlock(firstBlock).
|
||||
AddBlock(secondBlock)
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
err := b.Move(st, st, "1", model.Block_InnerFirst, []string{"2"})
|
||||
require.NoError(t, err)
|
||||
|
@ -350,7 +350,7 @@ func TestBasic_Move(t *testing.T) {
|
|||
AddBlock(newTextBlock("1", "", nil)).
|
||||
AddBlock(newTextBlock("2", "one", nil))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
err := b.Move(st, nil, "1", model.Block_Top, []string{"2"})
|
||||
require.NoError(t, err)
|
||||
|
@ -379,7 +379,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving non-root table block '"+block+"' leads to error", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -394,7 +394,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("no error on moving root table block", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -408,7 +408,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("no error on moving one row between another", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -422,7 +422,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving rows with incorrect position leads to error", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -435,7 +435,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving rows and some other blocks between another leads to error", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -448,7 +448,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving the row between itself leads to error", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -461,7 +461,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving table block from invalid table leads to error", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
st.Unlink("columns")
|
||||
|
||||
|
@ -477,7 +477,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving a block to '"+block+"' block leads to moving it under the table", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
|
||||
// when
|
||||
|
@ -492,7 +492,7 @@ func TestBasic_MoveTableBlocks(t *testing.T) {
|
|||
t.Run("moving a block to the invalid table leads to moving it under the table", func(t *testing.T) {
|
||||
// given
|
||||
sb := getSB()
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
st := sb.NewState()
|
||||
st.Unlink("columns")
|
||||
|
||||
|
@ -516,7 +516,7 @@ func TestBasic_MoveToAnotherObject(t *testing.T) {
|
|||
sb2 := smarttest.New("test2")
|
||||
sb2.AddBlock(simple.New(&model.Block{Id: "test2", ChildrenIds: []string{}}))
|
||||
|
||||
b := NewBasic(sb1, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb1, nil, converter.NewLayoutConverter(), nil)
|
||||
|
||||
srcState := sb1.NewState()
|
||||
destState := sb2.NewState()
|
||||
|
@ -551,7 +551,7 @@ func TestBasic_Replace(t *testing.T) {
|
|||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
|
||||
AddBlock(simple.New(&model.Block{Id: "2"}))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
newId, err := b.Replace(nil, "2", &model.Block{Content: &model.BlockContentOfText{Text: &model.BlockContentText{Text: "l"}}})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, newId)
|
||||
|
@ -561,7 +561,7 @@ func TestBasic_SetFields(t *testing.T) {
|
|||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
|
||||
AddBlock(simple.New(&model.Block{Id: "2"}))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
|
||||
fields := &types.Struct{
|
||||
Fields: map[string]*types.Value{
|
||||
|
@ -580,7 +580,7 @@ func TestBasic_Update(t *testing.T) {
|
|||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
|
||||
AddBlock(simple.New(&model.Block{Id: "2"}))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
|
||||
err := b.Update(nil, func(b simple.Block) error {
|
||||
b.Model().BackgroundColor = "test"
|
||||
|
@ -594,7 +594,7 @@ func TestBasic_SetDivStyle(t *testing.T) {
|
|||
sb := smarttest.New("test")
|
||||
sb.AddBlock(simple.New(&model.Block{Id: "test", ChildrenIds: []string{"2"}})).
|
||||
AddBlock(simple.New(&model.Block{Id: "2", Content: &model.BlockContentOfDiv{Div: &model.BlockContentDiv{}}}))
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
|
||||
err := b.SetDivStyle(nil, model.BlockContentDiv_Dots, "2")
|
||||
require.NoError(t, err)
|
||||
|
@ -614,7 +614,7 @@ func TestBasic_SetRelationKey(t *testing.T) {
|
|||
t.Run("correct", func(t *testing.T) {
|
||||
sb := smarttest.New("test")
|
||||
fillSb(sb)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
err := b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
|
||||
BlockId: "2",
|
||||
Key: "testRelKey",
|
||||
|
@ -636,7 +636,7 @@ func TestBasic_SetRelationKey(t *testing.T) {
|
|||
t.Run("not relation block", func(t *testing.T) {
|
||||
sb := smarttest.New("test")
|
||||
fillSb(sb)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
require.Error(t, b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
|
||||
BlockId: "1",
|
||||
Key: "key",
|
||||
|
@ -645,7 +645,7 @@ func TestBasic_SetRelationKey(t *testing.T) {
|
|||
t.Run("relation not found", func(t *testing.T) {
|
||||
sb := smarttest.New("test")
|
||||
fillSb(sb)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
require.Error(t, b.SetRelationKey(nil, pb.RpcBlockRelationSetKeyRequest{
|
||||
BlockId: "2",
|
||||
Key: "not exists",
|
||||
|
@ -661,7 +661,7 @@ func TestBasic_FeaturedRelationAdd(t *testing.T) {
|
|||
s.AddBundledRelationLinks(bundle.RelationKeyDescription)
|
||||
require.NoError(t, sb.Apply(s))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
newRel := []string{bundle.RelationKeyDescription.String(), bundle.RelationKeyName.String()}
|
||||
require.NoError(t, b.FeaturedRelationAdd(nil, newRel...))
|
||||
|
||||
|
@ -677,7 +677,7 @@ func TestBasic_FeaturedRelationRemove(t *testing.T) {
|
|||
template.WithDescription(s)
|
||||
require.NoError(t, sb.Apply(s))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
require.NoError(t, b.FeaturedRelationRemove(nil, bundle.RelationKeyDescription.String()))
|
||||
|
||||
res := sb.NewState()
|
||||
|
@ -714,7 +714,7 @@ func TestBasic_ReplaceLink(t *testing.T) {
|
|||
}
|
||||
require.NoError(t, sb.Apply(s))
|
||||
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil, nil)
|
||||
b := NewBasic(sb, nil, converter.NewLayoutConverter(), nil)
|
||||
require.NoError(t, b.ReplaceLink(oldId, newId))
|
||||
|
||||
res := sb.NewState()
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/state"
|
||||
|
@ -28,19 +27,6 @@ func (bs *basic) SetDetails(ctx session.Context, details []domain.Detail, showEv
|
|||
return err
|
||||
}
|
||||
|
||||
func (bs *basic) SetDetailsAndUpdateLastUsed(ctx session.Context, details []domain.Detail, showEvent bool) (err error) {
|
||||
var keys []domain.RelationKey
|
||||
keys, err = bs.setDetails(ctx, details, showEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ts := time.Now().Unix()
|
||||
for _, key := range keys {
|
||||
bs.lastUsedUpdater.UpdateLastUsedDate(bs.SpaceID(), key, ts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *basic) setDetails(ctx session.Context, details []domain.Detail, showEvent bool) (updatedKeys []domain.RelationKey, err error) {
|
||||
s := bs.NewStateCtx(ctx)
|
||||
|
||||
|
@ -67,23 +53,6 @@ func (bs *basic) UpdateDetails(ctx session.Context, update func(current *domain.
|
|||
return err
|
||||
}
|
||||
|
||||
func (bs *basic) UpdateDetailsAndLastUsed(ctx session.Context, update func(current *domain.Details) (*domain.Details, error)) error {
|
||||
oldDetails, newDetails, err := bs.updateDetails(ctx, update)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
diff, _ := domain.StructDiff(oldDetails, newDetails)
|
||||
if diff.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
ts := time.Now().Unix()
|
||||
for _, key := range diff.Keys() {
|
||||
bs.lastUsedUpdater.UpdateLastUsedDate(bs.SpaceID(), key, ts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs *basic) updateDetails(ctx session.Context, update func(current *domain.Details) (*domain.Details, error)) (oldDetails *domain.Details, newDetails *domain.Details, err error) {
|
||||
if update == nil {
|
||||
return nil, nil, fmt.Errorf("update function is nil")
|
||||
|
@ -180,7 +149,7 @@ func (bs *basic) validateDetailFormat(spaceID string, key domain.RelationKey, v
|
|||
}
|
||||
return nil
|
||||
case model.RelationFormat_status:
|
||||
vals, ok := v.TryStringList()
|
||||
vals, ok := v.TryWrapToStringList()
|
||||
if !ok {
|
||||
return fmt.Errorf("incorrect type: %v instead of string list", v)
|
||||
}
|
||||
|
@ -190,7 +159,7 @@ func (bs *basic) validateDetailFormat(spaceID string, key domain.RelationKey, v
|
|||
return bs.validateOptions(r, vals)
|
||||
|
||||
case model.RelationFormat_tag:
|
||||
vals, ok := v.TryStringList()
|
||||
vals, ok := v.TryWrapToStringList()
|
||||
if !ok {
|
||||
return fmt.Errorf("incorrect type: %v instead of string list", v)
|
||||
}
|
||||
|
@ -378,10 +347,6 @@ func (bs *basic) SetObjectTypesInState(s *state.State, objectTypeKeys []domain.T
|
|||
s.SetObjectTypeKeys(objectTypeKeys)
|
||||
removeInternalFlags(s)
|
||||
|
||||
if bs.CombinedDetails().GetInt64(bundle.RelationKeyOrigin) == int64(model.ObjectOrigin_none) {
|
||||
bs.lastUsedUpdater.UpdateLastUsedDate(bs.SpaceID(), objectTypeKeys[0], time.Now().Unix())
|
||||
}
|
||||
|
||||
toLayout, err := bs.getLayoutForType(objectTypeKeys[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("get layout for type %s: %w", objectTypeKeys[0], err)
|
||||
|
|
|
@ -4,10 +4,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/converter"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/lastused/mock_lastused"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
|
||||
"github.com/anyproto/anytype-heart/core/block/restriction"
|
||||
"github.com/anyproto/anytype-heart/core/domain"
|
||||
|
@ -18,10 +16,9 @@ import (
|
|||
)
|
||||
|
||||
type basicFixture struct {
|
||||
sb *smarttest.SmartTest
|
||||
store *spaceindex.StoreFixture
|
||||
lastUsed *mock_lastused.MockObjectUsageUpdater
|
||||
basic CommonOperations
|
||||
sb *smarttest.SmartTest
|
||||
store *spaceindex.StoreFixture
|
||||
basic CommonOperations
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -35,15 +32,13 @@ func newBasicFixture(t *testing.T) *basicFixture {
|
|||
sb.SetSpaceId(spaceId)
|
||||
|
||||
store := spaceindex.NewStoreFixture(t)
|
||||
lastUsed := mock_lastused.NewMockObjectUsageUpdater(t)
|
||||
|
||||
b := NewBasic(sb, store, converter.NewLayoutConverter(), nil, lastUsed)
|
||||
b := NewBasic(sb, store, converter.NewLayoutConverter(), nil)
|
||||
|
||||
return &basicFixture{
|
||||
sb: sb,
|
||||
store: store,
|
||||
lastUsed: lastUsed,
|
||||
basic: b,
|
||||
sb: sb,
|
||||
store: store,
|
||||
basic: b,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,7 +144,6 @@ func TestBasic_SetObjectTypesInState(t *testing.T) {
|
|||
// given
|
||||
f := newBasicFixture(t)
|
||||
|
||||
f.lastUsed.EXPECT().UpdateLastUsedDate(mock.Anything, bundle.TypeKeyTask, mock.Anything).Return().Once()
|
||||
f.store.AddObjects(t, []objectstore.TestObject{{
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyId: domain.String("ot-task"),
|
||||
|
|
|
@ -319,7 +319,7 @@ func TestExtractObjects(t *testing.T) {
|
|||
ObjectTypeUniqueKey: domain.MustUniqueKey(coresb.SmartBlockTypeObjectType, tc.typeKey).Marshal(),
|
||||
}
|
||||
ctx := session.NewContext()
|
||||
linkIds, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil, nil).ExtractBlocksToObjects(ctx, creator, ts, req)
|
||||
linkIds, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
gotBlockIds := []string{}
|
||||
|
@ -374,7 +374,7 @@ func TestExtractObjects(t *testing.T) {
|
|||
}},
|
||||
}
|
||||
ctx := session.NewContext()
|
||||
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil, nil).ExtractBlocksToObjects(ctx, creator, ts, req)
|
||||
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
|
||||
assert.NoError(t, err)
|
||||
var block *model.Block
|
||||
for _, block = range sb.Blocks() {
|
||||
|
@ -407,7 +407,7 @@ func TestExtractObjects(t *testing.T) {
|
|||
}},
|
||||
}
|
||||
ctx := session.NewContext()
|
||||
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil, nil).ExtractBlocksToObjects(ctx, creator, ts, req)
|
||||
_, err := NewBasic(sb, fixture.store, converter.NewLayoutConverter(), nil).ExtractBlocksToObjects(ctx, creator, ts, req)
|
||||
assert.NoError(t, err)
|
||||
var addedBlocks []*model.Block
|
||||
for _, message := range sb.Results.Events {
|
||||
|
|
|
@ -43,6 +43,7 @@ type StoreObject interface {
|
|||
ToggleMessageReaction(ctx context.Context, messageId string, emoji string) error
|
||||
DeleteMessage(ctx context.Context, messageId string) error
|
||||
SubscribeLastMessages(ctx context.Context, subId string, limit int, asyncInit bool) ([]*model.ChatMessage, int, error)
|
||||
MarkSeenHeads(heads []string)
|
||||
Unsubscribe(subId string) error
|
||||
}
|
||||
|
||||
|
@ -118,6 +119,10 @@ func (s *storeObject) onUpdate() {
|
|||
s.subscription.flush()
|
||||
}
|
||||
|
||||
func (s *storeObject) MarkSeenHeads(heads []string) {
|
||||
s.storeSource.MarkSeenHeads(heads)
|
||||
}
|
||||
|
||||
func (s *storeObject) GetMessagesByIds(ctx context.Context, messageIds []string) ([]*model.ChatMessage, error) {
|
||||
coll, err := s.store.Collection(ctx, collectionName)
|
||||
if err != nil {
|
||||
|
|
|
@ -35,7 +35,7 @@ type Dashboard struct {
|
|||
func NewDashboard(sb smartblock.SmartBlock, objectStore spaceindex.Store, layoutConverter converter.LayoutConverter) *Dashboard {
|
||||
return &Dashboard{
|
||||
SmartBlock: sb,
|
||||
AllOperations: basic.NewBasic(sb, objectStore, layoutConverter, nil, nil),
|
||||
AllOperations: basic.NewBasic(sb, objectStore, layoutConverter, nil),
|
||||
Collection: collection.NewCollection(sb, objectStore),
|
||||
objectStore: objectStore,
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ import (
|
|||
"github.com/anyproto/anytype-heart/core/block/editor/chatobject"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/converter"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/file"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/block/migration"
|
||||
"github.com/anyproto/anytype-heart/core/block/object/idresolver"
|
||||
|
@ -74,7 +73,6 @@ type ObjectFactory struct {
|
|||
fileReconciler reconciler.Reconciler
|
||||
objectDeleter ObjectDeleter
|
||||
deviceService deviceService
|
||||
lastUsedUpdater lastused.ObjectUsageUpdater
|
||||
spaceIdResolver idresolver.Resolver
|
||||
}
|
||||
|
||||
|
@ -107,7 +105,6 @@ func (f *ObjectFactory) Init(a *app.App) (err error) {
|
|||
f.objectDeleter = app.MustComponent[ObjectDeleter](a)
|
||||
f.fileReconciler = app.MustComponent[reconciler.Reconciler](a)
|
||||
f.deviceService = app.MustComponent[deviceService](a)
|
||||
f.lastUsedUpdater = app.MustComponent[lastused.ObjectUsageUpdater](a)
|
||||
f.spaceIdResolver = app.MustComponent[idresolver.Resolver](a)
|
||||
return nil
|
||||
}
|
||||
|
@ -215,7 +212,7 @@ func (f *ObjectFactory) New(space smartblock.Space, sbType coresb.SmartBlockType
|
|||
case coresb.SmartBlockTypeChatDerivedObject:
|
||||
return chatobject.New(sb, f.accountService, f.eventSender, f.objectStore.GetCrdtDb(space.Id()), spaceIndex), nil
|
||||
case coresb.SmartBlockTypeAccountObject:
|
||||
return accountobject.New(sb, f.accountService.Keys(), spaceIndex, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater, f.objectStore.GetCrdtDb(space.Id()), f.config), nil
|
||||
return accountobject.New(sb, f.accountService.Keys(), spaceIndex, f.layoutConverter, f.fileObjectService, f.objectStore.GetCrdtDb(space.Id()), f.config), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected smartblock type: %v", sbType)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ var fileRequiredRelations = append(pageRequiredRelations, []domain.RelationKey{
|
|||
|
||||
func (f *ObjectFactory) newFile(spaceId string, sb smartblock.SmartBlock) *File {
|
||||
store := f.objectStore.SpaceIndex(spaceId)
|
||||
basicComponent := basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater)
|
||||
basicComponent := basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService)
|
||||
return &File{
|
||||
SmartBlock: sb,
|
||||
ChangeReceiver: sb.(source.ChangeReceiver),
|
||||
|
|
|
@ -1,191 +1,15 @@
|
|||
package lastused
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/anyproto/any-sync/app/logger"
|
||||
"github.com/cheggaaa/mb/v3"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/domain"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
coresb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
|
||||
"github.com/anyproto/anytype-heart/space"
|
||||
"github.com/anyproto/anytype-heart/space/clientspace"
|
||||
)
|
||||
|
||||
const (
|
||||
CName = "object-usage-updater"
|
||||
|
||||
maxInstallationTime = 5 * time.Minute
|
||||
updateInterval = 5 * time.Second
|
||||
)
|
||||
|
||||
type Key interface {
|
||||
URL() string
|
||||
String() string
|
||||
}
|
||||
|
||||
type message struct {
|
||||
spaceId string
|
||||
key Key
|
||||
time int64
|
||||
}
|
||||
|
||||
var log = logger.NewNamed("update-last-used-date")
|
||||
|
||||
type ObjectUsageUpdater interface {
|
||||
app.ComponentRunnable
|
||||
|
||||
UpdateLastUsedDate(spaceId string, key Key, timeStamp int64)
|
||||
}
|
||||
|
||||
func New() ObjectUsageUpdater {
|
||||
return &updater{}
|
||||
}
|
||||
|
||||
type updater struct {
|
||||
store objectstore.ObjectStore
|
||||
spaceService space.Service
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
msgBatch *mb.MB[message]
|
||||
}
|
||||
|
||||
func (u *updater) Name() string {
|
||||
return CName
|
||||
}
|
||||
|
||||
func (u *updater) Init(a *app.App) error {
|
||||
u.store = app.MustComponent[objectstore.ObjectStore](a)
|
||||
u.spaceService = app.MustComponent[space.Service](a)
|
||||
u.msgBatch = mb.New[message](0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) Run(context.Context) error {
|
||||
u.ctx, u.cancel = context.WithCancel(context.Background())
|
||||
go u.lastUsedUpdateHandler()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) Close(context.Context) error {
|
||||
if u.cancel != nil {
|
||||
u.cancel()
|
||||
}
|
||||
if err := u.msgBatch.Close(); err != nil {
|
||||
log.Error("failed to close message batch", zap.Error(err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updater) UpdateLastUsedDate(spaceId string, key Key, ts int64) {
|
||||
if err := u.msgBatch.Add(u.ctx, message{spaceId: spaceId, key: key, time: ts}); err != nil {
|
||||
log.Error("failed to add last used date info to message batch", zap.Error(err), zap.String("key", key.String()))
|
||||
}
|
||||
}
|
||||
|
||||
func (u *updater) lastUsedUpdateHandler() {
|
||||
var (
|
||||
accumulator = make(map[string]map[Key]int64)
|
||||
lock sync.Mutex
|
||||
)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-u.ctx.Done():
|
||||
return
|
||||
case <-time.After(updateInterval):
|
||||
lock.Lock()
|
||||
if len(accumulator) == 0 {
|
||||
lock.Unlock()
|
||||
continue
|
||||
}
|
||||
for spaceId, keys := range accumulator {
|
||||
log.Debug("updating lastUsedDate for objects in space", zap.Int("objects num", len(keys)), zap.String("spaceId", spaceId))
|
||||
u.updateLastUsedDateForKeysInSpace(spaceId, keys)
|
||||
}
|
||||
accumulator = make(map[string]map[Key]int64)
|
||||
lock.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
msgs, err := u.msgBatch.Wait(u.ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
for _, msg := range msgs {
|
||||
if keys := accumulator[msg.spaceId]; keys != nil {
|
||||
keys[msg.key] = msg.time
|
||||
} else {
|
||||
keys = map[Key]int64{
|
||||
msg.key: msg.time,
|
||||
}
|
||||
accumulator[msg.spaceId] = keys
|
||||
}
|
||||
}
|
||||
lock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (u *updater) updateLastUsedDateForKeysInSpace(spaceId string, keys map[Key]int64) {
|
||||
spc, err := u.spaceService.Get(u.ctx, spaceId)
|
||||
if err != nil {
|
||||
log.Error("failed to get space", zap.String("spaceId", spaceId), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
for key, timeStamp := range keys {
|
||||
if err = u.updateLastUsedDate(spc, key, timeStamp); err != nil {
|
||||
log.Error("failed to update last used date", zap.String("spaceId", spaceId), zap.String("key", key.String()), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *updater) updateLastUsedDate(spc clientspace.Space, key Key, ts int64) error {
|
||||
uk, err := domain.UnmarshalUniqueKey(key.URL())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshall key: %w", err)
|
||||
}
|
||||
|
||||
if uk.SmartblockType() != coresb.SmartBlockTypeObjectType && uk.SmartblockType() != coresb.SmartBlockTypeRelation {
|
||||
return fmt.Errorf("cannot update lastUsedDate for object with invalid smartBlock type. Only object types and relations are expected")
|
||||
}
|
||||
|
||||
details, err := u.store.SpaceIndex(spc.Id()).GetObjectByUniqueKey(uk)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get details: %w", err)
|
||||
}
|
||||
|
||||
id := details.GetString(bundle.RelationKeyId)
|
||||
if id == "" {
|
||||
return fmt.Errorf("failed to get id from details: %w", err)
|
||||
}
|
||||
|
||||
if err = spc.DoCtx(u.ctx, id, func(sb smartblock.SmartBlock) error {
|
||||
st := sb.NewState()
|
||||
st.SetLocalDetail(bundle.RelationKeyLastUsedDate, domain.Int64(ts))
|
||||
return sb.Apply(st)
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to set lastUsedDate to object: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
const maxInstallationTime = 5 * time.Minute
|
||||
|
||||
func SetLastUsedDateForInitialObjectType(id string, details *domain.Details) {
|
||||
if !strings.HasPrefix(id, addr.BundledObjectTypeURLPrefix) || details == nil {
|
||||
|
|
|
@ -1,22 +1,13 @@
|
|||
package lastused
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
|
||||
"github.com/anyproto/anytype-heart/core/domain"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
|
||||
"github.com/anyproto/anytype-heart/space/clientspace"
|
||||
"github.com/anyproto/anytype-heart/space/clientspace/mock_clientspace"
|
||||
)
|
||||
|
||||
func TestSetLastUsedDateForInitialType(t *testing.T) {
|
||||
|
@ -56,82 +47,3 @@ func TestSetLastUsedDateForInitialType(t *testing.T) {
|
|||
assert.True(t, isLastUsedDateGreater(detailMap[bundle.TypeKeyCollection.BundledURL()], detailMap[bundle.TypeKeyDiaryEntry.BundledURL()]))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateLastUsedDate(t *testing.T) {
|
||||
const spaceId = "space"
|
||||
|
||||
ts := time.Now().Unix()
|
||||
|
||||
isLastUsedDateRecent := func(details *domain.Details, deltaSeconds int64) bool {
|
||||
return details.GetInt64(bundle.RelationKeyLastUsedDate)+deltaSeconds > time.Now().Unix()
|
||||
}
|
||||
|
||||
store := objectstore.NewStoreFixture(t)
|
||||
store.AddObjects(t, spaceId, []objectstore.TestObject{
|
||||
{
|
||||
bundle.RelationKeyId: domain.String(bundle.RelationKeyCamera.URL()),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyUniqueKey: domain.String(bundle.RelationKeyCamera.URL()),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: domain.String(bundle.TypeKeyDiaryEntry.URL()),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyUniqueKey: domain.String(bundle.TypeKeyDiaryEntry.URL()),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: domain.String("rel-custom"),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyUniqueKey: domain.String("rel-custom"),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: domain.String("opt-done"),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyUniqueKey: domain.String("opt-done"),
|
||||
},
|
||||
})
|
||||
|
||||
u := updater{store: store}
|
||||
|
||||
getSpace := func() clientspace.Space {
|
||||
spc := mock_clientspace.NewMockSpace(t)
|
||||
spc.EXPECT().Id().Return(spaceId)
|
||||
spc.EXPECT().DoCtx(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(_ context.Context, id string, apply func(smartblock.SmartBlock) error) error {
|
||||
sb := smarttest.New(id)
|
||||
err := apply(sb)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, isLastUsedDateRecent(sb.LocalDetails(), 5))
|
||||
return nil
|
||||
})
|
||||
return spc
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
key Key
|
||||
getSpace func() clientspace.Space
|
||||
isErrorExpected bool
|
||||
}{
|
||||
{"built-in relation", bundle.RelationKeyCamera, getSpace, false},
|
||||
{"built-in type", bundle.TypeKeyDiaryEntry, getSpace, false},
|
||||
{"custom relation", domain.RelationKey("custom"), getSpace, false},
|
||||
{"option", domain.TypeKey("opt-done"), func() clientspace.Space {
|
||||
spc := mock_clientspace.NewMockSpace(t)
|
||||
return spc
|
||||
}, true},
|
||||
{"type that is not in store", bundle.TypeKeyAudio, func() clientspace.Space {
|
||||
spc := mock_clientspace.NewMockSpace(t)
|
||||
spc.EXPECT().Id().Return(spaceId)
|
||||
return spc
|
||||
}, true},
|
||||
} {
|
||||
t.Run("update lastUsedDate of "+tc.name, func(t *testing.T) {
|
||||
err := u.updateLastUsedDate(tc.getSpace(), tc.key, ts)
|
||||
if tc.isErrorExpected {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,258 +0,0 @@
|
|||
// Code generated by mockery. DO NOT EDIT.
|
||||
|
||||
package mock_lastused
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
app "github.com/anyproto/any-sync/app"
|
||||
|
||||
lastused "github.com/anyproto/anytype-heart/core/block/editor/lastused"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockObjectUsageUpdater is an autogenerated mock type for the ObjectUsageUpdater type
|
||||
type MockObjectUsageUpdater struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockObjectUsageUpdater_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockObjectUsageUpdater) EXPECT() *MockObjectUsageUpdater_Expecter {
|
||||
return &MockObjectUsageUpdater_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Close provides a mock function with given fields: ctx
|
||||
func (_m *MockObjectUsageUpdater) Close(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Close")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockObjectUsageUpdater_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
|
||||
type MockObjectUsageUpdater_Close_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Close is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
func (_e *MockObjectUsageUpdater_Expecter) Close(ctx interface{}) *MockObjectUsageUpdater_Close_Call {
|
||||
return &MockObjectUsageUpdater_Close_Call{Call: _e.mock.On("Close", ctx)}
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_Close_Call) Run(run func(ctx context.Context)) *MockObjectUsageUpdater_Close_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_Close_Call) Return(err error) *MockObjectUsageUpdater_Close_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_Close_Call) RunAndReturn(run func(context.Context) error) *MockObjectUsageUpdater_Close_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Init provides a mock function with given fields: a
|
||||
func (_m *MockObjectUsageUpdater) Init(a *app.App) error {
|
||||
ret := _m.Called(a)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Init")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*app.App) error); ok {
|
||||
r0 = rf(a)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockObjectUsageUpdater_Init_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Init'
|
||||
type MockObjectUsageUpdater_Init_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Init is a helper method to define mock.On call
|
||||
// - a *app.App
|
||||
func (_e *MockObjectUsageUpdater_Expecter) Init(a interface{}) *MockObjectUsageUpdater_Init_Call {
|
||||
return &MockObjectUsageUpdater_Init_Call{Call: _e.mock.On("Init", a)}
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_Init_Call) Run(run func(a *app.App)) *MockObjectUsageUpdater_Init_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(*app.App))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_Init_Call) Return(err error) *MockObjectUsageUpdater_Init_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_Init_Call) RunAndReturn(run func(*app.App) error) *MockObjectUsageUpdater_Init_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Name provides a mock function with given fields:
|
||||
func (_m *MockObjectUsageUpdater) Name() string {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Name")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockObjectUsageUpdater_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
|
||||
type MockObjectUsageUpdater_Name_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Name is a helper method to define mock.On call
|
||||
func (_e *MockObjectUsageUpdater_Expecter) Name() *MockObjectUsageUpdater_Name_Call {
|
||||
return &MockObjectUsageUpdater_Name_Call{Call: _e.mock.On("Name")}
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_Name_Call) Run(run func()) *MockObjectUsageUpdater_Name_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_Name_Call) Return(name string) *MockObjectUsageUpdater_Name_Call {
|
||||
_c.Call.Return(name)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_Name_Call) RunAndReturn(run func() string) *MockObjectUsageUpdater_Name_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Run provides a mock function with given fields: ctx
|
||||
func (_m *MockObjectUsageUpdater) Run(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Run")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockObjectUsageUpdater_Run_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Run'
|
||||
type MockObjectUsageUpdater_Run_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Run is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
func (_e *MockObjectUsageUpdater_Expecter) Run(ctx interface{}) *MockObjectUsageUpdater_Run_Call {
|
||||
return &MockObjectUsageUpdater_Run_Call{Call: _e.mock.On("Run", ctx)}
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_Run_Call) Run(run func(ctx context.Context)) *MockObjectUsageUpdater_Run_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_Run_Call) Return(err error) *MockObjectUsageUpdater_Run_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_Run_Call) RunAndReturn(run func(context.Context) error) *MockObjectUsageUpdater_Run_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateLastUsedDate provides a mock function with given fields: spaceId, key, timeStamp
|
||||
func (_m *MockObjectUsageUpdater) UpdateLastUsedDate(spaceId string, key lastused.Key, timeStamp int64) {
|
||||
_m.Called(spaceId, key, timeStamp)
|
||||
}
|
||||
|
||||
// MockObjectUsageUpdater_UpdateLastUsedDate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateLastUsedDate'
|
||||
type MockObjectUsageUpdater_UpdateLastUsedDate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateLastUsedDate is a helper method to define mock.On call
|
||||
// - spaceId string
|
||||
// - key lastused.Key
|
||||
// - timeStamp int64
|
||||
func (_e *MockObjectUsageUpdater_Expecter) UpdateLastUsedDate(spaceId interface{}, key interface{}, timeStamp interface{}) *MockObjectUsageUpdater_UpdateLastUsedDate_Call {
|
||||
return &MockObjectUsageUpdater_UpdateLastUsedDate_Call{Call: _e.mock.On("UpdateLastUsedDate", spaceId, key, timeStamp)}
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_UpdateLastUsedDate_Call) Run(run func(spaceId string, key lastused.Key, timeStamp int64)) *MockObjectUsageUpdater_UpdateLastUsedDate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(lastused.Key), args[2].(int64))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_UpdateLastUsedDate_Call) Return() *MockObjectUsageUpdater_UpdateLastUsedDate_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockObjectUsageUpdater_UpdateLastUsedDate_Call) RunAndReturn(run func(string, lastused.Key, int64)) *MockObjectUsageUpdater_UpdateLastUsedDate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockObjectUsageUpdater creates a new instance of MockObjectUsageUpdater. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockObjectUsageUpdater(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockObjectUsageUpdater {
|
||||
mock := &MockObjectUsageUpdater{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
|
@ -59,7 +59,7 @@ func (f *ObjectFactory) newPage(spaceId string, sb smartblock.SmartBlock) *Page
|
|||
return &Page{
|
||||
SmartBlock: sb,
|
||||
ChangeReceiver: sb.(source.ChangeReceiver),
|
||||
AllOperations: basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater),
|
||||
AllOperations: basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService),
|
||||
IHistory: basic.NewHistory(sb),
|
||||
Text: stext.NewText(
|
||||
sb,
|
||||
|
|
|
@ -30,7 +30,7 @@ type participant struct {
|
|||
}
|
||||
|
||||
func (f *ObjectFactory) newParticipant(spaceId string, sb smartblock.SmartBlock, spaceIndex spaceindex.Store) *participant {
|
||||
basicComponent := basic.NewBasic(sb, spaceIndex, f.layoutConverter, nil, f.lastUsedUpdater)
|
||||
basicComponent := basic.NewBasic(sb, spaceIndex, f.layoutConverter, nil)
|
||||
return &participant{
|
||||
SmartBlock: sb,
|
||||
DetailsUpdatable: basicComponent,
|
||||
|
|
|
@ -120,7 +120,7 @@ func TestParticipant_Init(t *testing.T) {
|
|||
bundle.RelationKeyName: domain.String("test"),
|
||||
}})
|
||||
|
||||
basicComponent := basic.NewBasic(sb, store, nil, nil, nil)
|
||||
basicComponent := basic.NewBasic(sb, store, nil, nil)
|
||||
p := &participant{
|
||||
SmartBlock: sb,
|
||||
DetailsUpdatable: basicComponent,
|
||||
|
@ -147,7 +147,7 @@ func TestParticipant_Init(t *testing.T) {
|
|||
sb := smarttest.New("root")
|
||||
store := newStoreFixture(t)
|
||||
|
||||
basicComponent := basic.NewBasic(sb, store, nil, nil, nil)
|
||||
basicComponent := basic.NewBasic(sb, store, nil, nil)
|
||||
p := &participant{
|
||||
SmartBlock: sb,
|
||||
DetailsUpdatable: basicComponent,
|
||||
|
@ -196,7 +196,7 @@ func newStoreFixture(t *testing.T) *spaceindex.StoreFixture {
|
|||
func newParticipantTest(t *testing.T) (*participant, error) {
|
||||
sb := smarttest.New("root")
|
||||
store := newStoreFixture(t)
|
||||
basicComponent := basic.NewBasic(sb, store, nil, nil, nil)
|
||||
basicComponent := basic.NewBasic(sb, store, nil, nil)
|
||||
p := &participant{
|
||||
SmartBlock: sb,
|
||||
DetailsUpdatable: basicComponent,
|
||||
|
|
|
@ -40,7 +40,7 @@ func (f *ObjectFactory) newProfile(spaceId string, sb smartblock.SmartBlock) *Pr
|
|||
fileComponent := file.NewFile(sb, f.fileBlockService, f.picker, f.processService, f.fileUploaderService)
|
||||
return &Profile{
|
||||
SmartBlock: sb,
|
||||
AllOperations: basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater),
|
||||
AllOperations: basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService),
|
||||
IHistory: basic.NewHistory(sb),
|
||||
Text: stext.NewText(
|
||||
sb,
|
||||
|
|
|
@ -262,13 +262,6 @@ func (st *SmartTest) SetDetails(ctx session.Context, details []domain.Detail, sh
|
|||
return
|
||||
}
|
||||
|
||||
func (st *SmartTest) SetDetailsAndUpdateLastUsed(ctx session.Context, details []domain.Detail, showEvent bool) (err error) {
|
||||
for _, detail := range details {
|
||||
st.Results.LastUsedUpdates = append(st.Results.LastUsedUpdates, string(detail.Key))
|
||||
}
|
||||
return st.SetDetails(ctx, details, showEvent)
|
||||
}
|
||||
|
||||
func (st *SmartTest) UpdateDetails(ctx session.Context, update func(current *domain.Details) (*domain.Details, error)) (err error) {
|
||||
details := st.Doc.(*state.State).CombinedDetails()
|
||||
if details == nil {
|
||||
|
@ -282,31 +275,6 @@ func (st *SmartTest) UpdateDetails(ctx session.Context, update func(current *dom
|
|||
return nil
|
||||
}
|
||||
|
||||
func (st *SmartTest) UpdateDetailsAndLastUsed(ctx session.Context, update func(current *domain.Details) (*domain.Details, error)) (err error) {
|
||||
details := st.Doc.(*state.State).CombinedDetails()
|
||||
if details == nil {
|
||||
details = domain.NewDetails()
|
||||
}
|
||||
oldDetails := details.Copy()
|
||||
|
||||
newDetails, err := update(details)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
diff, _ := domain.StructDiff(oldDetails, newDetails)
|
||||
if diff == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
st.Doc.(*state.State).SetDetails(newDetails)
|
||||
|
||||
for k, _ := range diff.Iterate() {
|
||||
st.Results.LastUsedUpdates = append(st.Results.LastUsedUpdates, string(k))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *SmartTest) Init(ctx *smartblock.InitContext) (err error) {
|
||||
if ctx.State == nil {
|
||||
ctx.State = st.NewState()
|
||||
|
@ -453,6 +421,4 @@ func (st *SmartTest) Update(ctx session.Context, apply func(b simple.Block) erro
|
|||
type Results struct {
|
||||
Events [][]simple.EventMessage
|
||||
Applies [][]*model.Block
|
||||
|
||||
LastUsedUpdates []string
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -136,7 +137,67 @@ type State struct {
|
|||
|
||||
groupId string
|
||||
noObjectType bool
|
||||
originalCreatedTimestamp int64 // pass here from snapshots when importing objects
|
||||
originalCreatedTimestamp int64 // pass here from snapshots when importing objects or used for derived objects such as relations, types and etc
|
||||
}
|
||||
|
||||
type RelationsByLayout map[model.ObjectTypeLayout][]domain.RelationKey
|
||||
|
||||
type Filters struct {
|
||||
RelationsWhiteList RelationsByLayout
|
||||
RemoveBlocks bool
|
||||
}
|
||||
|
||||
// Filter should be called with state copy
|
||||
func (s *State) Filter(filters *Filters) *State {
|
||||
if filters == nil {
|
||||
return s
|
||||
}
|
||||
if filters.RemoveBlocks {
|
||||
s.filterBlocks()
|
||||
}
|
||||
if len(filters.RelationsWhiteList) > 0 {
|
||||
s.filterRelations(filters)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *State) filterBlocks() {
|
||||
resultBlocks := make(map[string]simple.Block)
|
||||
if block, ok := s.blocks[s.rootId]; ok {
|
||||
resultBlocks[s.rootId] = block
|
||||
}
|
||||
s.blocks = resultBlocks
|
||||
}
|
||||
|
||||
func (s *State) filterRelations(filters *Filters) {
|
||||
resultDetails := domain.NewDetails()
|
||||
layout, _ := s.Layout()
|
||||
relationKeys := filters.RelationsWhiteList[layout]
|
||||
var updatedRelationLinks pbtypes.RelationLinks
|
||||
for key, value := range s.details.Iterate() {
|
||||
if slices.Contains(relationKeys, key) {
|
||||
resultDetails.Set(key, value)
|
||||
updatedRelationLinks = append(updatedRelationLinks, s.relationLinks.Get(key.String()))
|
||||
continue
|
||||
}
|
||||
}
|
||||
s.details = resultDetails
|
||||
if resultDetails.Len() == 0 {
|
||||
s.details = nil
|
||||
}
|
||||
resultLocalDetails := domain.NewDetails()
|
||||
for key, value := range s.localDetails.Iterate() {
|
||||
if slices.Contains(relationKeys, key) {
|
||||
resultLocalDetails.Set(key, value)
|
||||
updatedRelationLinks = append(updatedRelationLinks, s.relationLinks.Get(key.String()))
|
||||
continue
|
||||
}
|
||||
}
|
||||
s.localDetails = resultLocalDetails
|
||||
if resultLocalDetails.Len() == 0 {
|
||||
s.localDetails = nil
|
||||
}
|
||||
s.relationLinks = updatedRelationLinks
|
||||
}
|
||||
|
||||
func (s *State) MigrationVersion() uint32 {
|
||||
|
@ -1701,9 +1762,6 @@ func (s *State) AddRelationLinks(links ...*model.RelationLink) {
|
|||
for _, l := range links {
|
||||
if !relLinks.Has(l.Key) {
|
||||
relLinks = append(relLinks, l)
|
||||
if l.Format == model.RelationFormat_checkbox {
|
||||
s.SetDetail(domain.RelationKey(l.Key), domain.Bool(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
s.relationLinks = relLinks
|
||||
|
|
|
@ -2976,43 +2976,125 @@ func TestState_AddRelationLinks(t *testing.T) {
|
|||
assert.True(t, s.GetRelationLinks().Has("existingLink"))
|
||||
assert.Len(t, s.GetRelationLinks(), 1)
|
||||
})
|
||||
t.Run("add checkbox link", func(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
t.Run("remove blocks", func(t *testing.T) {
|
||||
// given
|
||||
s := &State{}
|
||||
checkboxLink := &model.RelationLink{
|
||||
Key: "checkboxLink",
|
||||
Format: model.RelationFormat_checkbox,
|
||||
}
|
||||
st := NewDoc("root", map[string]simple.Block{
|
||||
"root": base.NewBase(&model.Block{Id: "root", ChildrenIds: []string{"2"}}),
|
||||
"2": base.NewBase(&model.Block{Id: "2"}),
|
||||
}).(*State)
|
||||
st.AddDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyCoverType: domain.Int64(1),
|
||||
bundle.RelationKeyName: domain.String("name"),
|
||||
bundle.RelationKeyAssignee: domain.String("assignee"),
|
||||
bundle.RelationKeyLayout: domain.Int64(model.ObjectType_todo),
|
||||
}))
|
||||
st.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyCoverType.String(),
|
||||
Format: model.RelationFormat_number,
|
||||
},
|
||||
&model.RelationLink{
|
||||
Key: bundle.RelationKeyName.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
},
|
||||
&model.RelationLink{
|
||||
Key: bundle.RelationKeyAssignee.String(),
|
||||
Format: model.RelationFormat_object,
|
||||
},
|
||||
&model.RelationLink{
|
||||
Key: bundle.RelationKeyLayout.String(),
|
||||
Format: model.RelationFormat_number,
|
||||
},
|
||||
)
|
||||
|
||||
// when
|
||||
s.AddRelationLinks(checkboxLink)
|
||||
filteredState := st.Filter(&Filters{RemoveBlocks: true})
|
||||
|
||||
// then
|
||||
relLinks := s.GetRelationLinks()
|
||||
assert.Equal(t, 1, len(relLinks))
|
||||
assert.True(t, relLinks.Has("checkboxLink"))
|
||||
detailValue := s.Details().Get("checkboxLink")
|
||||
assert.Equal(t, domain.Bool(false), detailValue)
|
||||
assert.Len(t, filteredState.blocks, 1)
|
||||
assert.NotNil(t, filteredState.blocks["root"])
|
||||
})
|
||||
t.Run("multi links", func(t *testing.T) {
|
||||
t.Run("filter relations by white list", func(t *testing.T) {
|
||||
// given
|
||||
s := &State{}
|
||||
link1 := &model.RelationLink{
|
||||
Key: "link1",
|
||||
Format: model.RelationFormat_shorttext,
|
||||
}
|
||||
link2 := &model.RelationLink{
|
||||
Key: "link2",
|
||||
Format: model.RelationFormat_checkbox,
|
||||
}
|
||||
st := NewDoc("root", map[string]simple.Block{
|
||||
"root": base.NewBase(&model.Block{Id: "root", ChildrenIds: []string{"2"}}),
|
||||
"2": base.NewBase(&model.Block{Id: "2"}),
|
||||
}).(*State)
|
||||
st.AddDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyCoverType: domain.Int64(1),
|
||||
bundle.RelationKeyName: domain.String("name"),
|
||||
bundle.RelationKeyAssignee: domain.String("assignee"),
|
||||
bundle.RelationKeyLayout: domain.Int64(model.ObjectType_todo),
|
||||
}))
|
||||
st.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyCoverType.String(),
|
||||
Format: model.RelationFormat_number,
|
||||
},
|
||||
&model.RelationLink{
|
||||
Key: bundle.RelationKeyName.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
},
|
||||
&model.RelationLink{
|
||||
Key: bundle.RelationKeyAssignee.String(),
|
||||
Format: model.RelationFormat_object,
|
||||
},
|
||||
&model.RelationLink{
|
||||
Key: bundle.RelationKeyLayout.String(),
|
||||
Format: model.RelationFormat_number,
|
||||
},
|
||||
)
|
||||
|
||||
// when
|
||||
s.AddRelationLinks(link1, link2)
|
||||
filteredState := st.Filter(&Filters{RelationsWhiteList: map[model.ObjectTypeLayout][]domain.RelationKey{
|
||||
model.ObjectType_todo: {bundle.RelationKeyAssignee},
|
||||
}})
|
||||
|
||||
// then
|
||||
relLinks := s.GetRelationLinks()
|
||||
assert.Equal(t, 2, len(relLinks))
|
||||
assert.True(t, relLinks.Has("link1"))
|
||||
assert.True(t, relLinks.Has("link2"))
|
||||
assert.Equal(t, filteredState.details.Len(), 1)
|
||||
assert.Equal(t, filteredState.localDetails.Len(), 0)
|
||||
assert.Len(t, filteredState.relationLinks, 1)
|
||||
assert.Equal(t, bundle.RelationKeyAssignee.String(), filteredState.relationLinks[0].Key)
|
||||
})
|
||||
t.Run("empty white list relations", func(t *testing.T) {
|
||||
// given
|
||||
st := NewDoc("root", map[string]simple.Block{
|
||||
"root": base.NewBase(&model.Block{Id: "root", ChildrenIds: []string{"2"}}),
|
||||
"2": base.NewBase(&model.Block{Id: "2"}),
|
||||
}).(*State)
|
||||
st.AddDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyCoverType: domain.Int64(1),
|
||||
bundle.RelationKeyName: domain.String("name"),
|
||||
bundle.RelationKeyAssignee: domain.String("assignee"),
|
||||
bundle.RelationKeyLayout: domain.Int64(model.ObjectType_todo),
|
||||
}))
|
||||
st.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyCoverType.String(),
|
||||
Format: model.RelationFormat_number,
|
||||
},
|
||||
&model.RelationLink{
|
||||
Key: bundle.RelationKeyName.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
},
|
||||
&model.RelationLink{
|
||||
Key: bundle.RelationKeyAssignee.String(),
|
||||
Format: model.RelationFormat_object,
|
||||
},
|
||||
&model.RelationLink{
|
||||
Key: bundle.RelationKeyLayout.String(),
|
||||
Format: model.RelationFormat_number,
|
||||
},
|
||||
)
|
||||
|
||||
// when
|
||||
filteredState := st.Filter(&Filters{RelationsWhiteList: map[model.ObjectTypeLayout][]domain.RelationKey{
|
||||
model.ObjectType_todo: {},
|
||||
}})
|
||||
|
||||
// then
|
||||
assert.Equal(t, filteredState.details.Len(), 0)
|
||||
assert.Equal(t, filteredState.localDetails.Len(), 0)
|
||||
assert.Len(t, filteredState.relationLinks, 0)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,72 +1,60 @@
|
|||
package debug
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/debug/treearchive"
|
||||
)
|
||||
|
||||
func TestBuildFast(t *testing.T) {
|
||||
// Specify the directory you want to iterate
|
||||
dir := "./testdata"
|
||||
|
||||
// Read the directory
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read dir: %s", err)
|
||||
}
|
||||
|
||||
// Iterate over the files
|
||||
for _, file := range files {
|
||||
t.Run(file.Name(), func(t *testing.T) {
|
||||
filePath := filepath.Join(dir, file.Name())
|
||||
|
||||
// open the file
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open file: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
testBuildFast(t, filePath)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testBuildFast(b *testing.T, filepath string) {
|
||||
// todo: replace with less heavy tree
|
||||
archive, err := treearchive.Open(filepath)
|
||||
if err != nil {
|
||||
require.NoError(b, err)
|
||||
}
|
||||
defer archive.Close()
|
||||
|
||||
importer := treearchive.NewTreeImporter(archive.ListStorage(), archive.TreeStorage())
|
||||
|
||||
err = importer.Import(false, "")
|
||||
if err != nil {
|
||||
log.Fatal("can't import the tree", err)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
_, err = importer.State()
|
||||
if err != nil {
|
||||
log.Fatal("can't build state:", err)
|
||||
}
|
||||
b.Logf("fast build took %s", time.Since(start))
|
||||
|
||||
importer2 := treearchive.NewTreeImporter(archive.ListStorage(), archive.TreeStorage())
|
||||
|
||||
err = importer2.Import(false, "")
|
||||
if err != nil {
|
||||
log.Fatal("can't import the tree", err)
|
||||
}
|
||||
|
||||
}
|
||||
// TODO: revive at some point
|
||||
// func TestBuildFast(t *testing.T) {
|
||||
// // Specify the directory you want to iterate
|
||||
// dir := "./testdata"
|
||||
//
|
||||
// // Read the directory
|
||||
// files, err := ioutil.ReadDir(dir)
|
||||
// if err != nil {
|
||||
// t.Fatalf("Failed to read dir: %s", err)
|
||||
// }
|
||||
//
|
||||
// // Iterate over the files
|
||||
// for _, file := range files {
|
||||
// t.Run(file.Name(), func(t *testing.T) {
|
||||
// filePath := filepath.Join(dir, file.Name())
|
||||
//
|
||||
// // open the file
|
||||
// f, err := os.Open(filePath)
|
||||
// if err != nil {
|
||||
// t.Fatalf("Failed to open file: %s", err)
|
||||
// }
|
||||
// defer f.Close()
|
||||
//
|
||||
// testBuildFast(t, filePath)
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func testBuildFast(b *testing.T, filepath string) {
|
||||
// // todo: replace with less heavy tree
|
||||
// archive, err := treearchive.Open(filepath)
|
||||
// if err != nil {
|
||||
// require.NoError(b, err)
|
||||
// }
|
||||
// defer archive.Close()
|
||||
//
|
||||
// importer := exporter.NewTreeImporter(archive.ListStorage(), archive.TreeStorage())
|
||||
//
|
||||
// err = importer.Import(false, "")
|
||||
// if err != nil {
|
||||
// log.Fatal("can't import the tree", err)
|
||||
// }
|
||||
//
|
||||
// start := time.Now()
|
||||
// _, err = importer.State()
|
||||
// if err != nil {
|
||||
// log.Fatal("can't build state:", err)
|
||||
// }
|
||||
// b.Logf("fast build took %s", time.Since(start))
|
||||
//
|
||||
// importer2 := exporter.NewTreeImporter(archive.ListStorage(), archive.TreeStorage())
|
||||
//
|
||||
// err = importer2.Import(false, "")
|
||||
// if err != nil {
|
||||
// log.Fatal("can't import the tree", err)
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
|
|
@ -47,10 +47,6 @@ func (stx *StoreStateTx) NextOrder(prev string) string {
|
|||
return lexId.Next(prev)
|
||||
}
|
||||
|
||||
func (stx *StoreStateTx) NextBeforeOrder(prev string, before string) (string, error) {
|
||||
return lexId.NextBefore(prev, before)
|
||||
}
|
||||
|
||||
func (stx *StoreStateTx) SetOrder(changeId, order string) (err error) {
|
||||
stx.arena.Reset()
|
||||
obj := stx.arena.NewObject()
|
||||
|
|
|
@ -29,7 +29,7 @@ func NewWidgetObject(
|
|||
objectStore spaceindex.Store,
|
||||
layoutConverter converter.LayoutConverter,
|
||||
) *WidgetObject {
|
||||
bs := basic.NewBasic(sb, objectStore, layoutConverter, nil, nil)
|
||||
bs := basic.NewBasic(sb, objectStore, layoutConverter, nil)
|
||||
return &WidgetObject{
|
||||
SmartBlock: sb,
|
||||
Movable: bs,
|
||||
|
|
|
@ -36,7 +36,7 @@ type Workspaces struct {
|
|||
func (f *ObjectFactory) newWorkspace(sb smartblock.SmartBlock, store spaceindex.Store) *Workspaces {
|
||||
w := &Workspaces{
|
||||
SmartBlock: sb,
|
||||
AllOperations: basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService, f.lastUsedUpdater),
|
||||
AllOperations: basic.NewBasic(sb, store, f.layoutConverter, f.fileObjectService),
|
||||
IHistory: basic.NewHistory(sb),
|
||||
Text: stext.NewText(
|
||||
sb,
|
||||
|
|
|
@ -134,51 +134,82 @@ func (e *export) finishWithNotification(spaceId string, exportFormat model.Expor
|
|||
}, nil)
|
||||
}
|
||||
|
||||
type Doc struct {
|
||||
Details *domain.Details
|
||||
isLink bool
|
||||
}
|
||||
|
||||
type Docs map[string]*Doc
|
||||
|
||||
func (d Docs) transformToDetailsMap() map[string]*domain.Details {
|
||||
details := make(map[string]*domain.Details, len(d))
|
||||
for id, doc := range d {
|
||||
details[id] = doc.Details
|
||||
}
|
||||
return details
|
||||
}
|
||||
|
||||
type exportContext struct {
|
||||
spaceId string
|
||||
docs map[string]*domain.Details
|
||||
includeArchive bool
|
||||
includeNested bool
|
||||
includeFiles bool
|
||||
format model.ExportFormat
|
||||
isJson bool
|
||||
reqIds []string
|
||||
zip bool
|
||||
path string
|
||||
spaceId string
|
||||
docs Docs
|
||||
includeArchive bool
|
||||
includeNested bool
|
||||
includeFiles bool
|
||||
format model.ExportFormat
|
||||
isJson bool
|
||||
reqIds []string
|
||||
zip bool
|
||||
path string
|
||||
linkStateFilters *state.Filters
|
||||
isLinkProcess bool
|
||||
includeBackLinks bool
|
||||
|
||||
*export
|
||||
}
|
||||
|
||||
func newExportContext(e *export, req pb.RpcObjectListExportRequest) *exportContext {
|
||||
return &exportContext{
|
||||
path: req.Path,
|
||||
spaceId: req.SpaceId,
|
||||
docs: map[string]*domain.Details{},
|
||||
includeArchive: req.IncludeArchived,
|
||||
includeNested: req.IncludeNested,
|
||||
includeFiles: req.IncludeFiles,
|
||||
format: req.Format,
|
||||
isJson: req.IsJson,
|
||||
reqIds: req.ObjectIds,
|
||||
zip: req.Zip,
|
||||
export: e,
|
||||
ec := &exportContext{
|
||||
path: req.Path,
|
||||
spaceId: req.SpaceId,
|
||||
docs: map[string]*Doc{},
|
||||
includeArchive: req.IncludeArchived,
|
||||
includeNested: req.IncludeNested,
|
||||
includeFiles: req.IncludeFiles,
|
||||
format: req.Format,
|
||||
isJson: req.IsJson,
|
||||
reqIds: req.ObjectIds,
|
||||
zip: req.Zip,
|
||||
linkStateFilters: pbFiltersToState(req.LinksStateFilters),
|
||||
includeBackLinks: req.IncludeBacklinks,
|
||||
export: e,
|
||||
}
|
||||
return ec
|
||||
}
|
||||
|
||||
func (e *exportContext) copy() *exportContext {
|
||||
return &exportContext{
|
||||
spaceId: e.spaceId,
|
||||
docs: e.docs,
|
||||
includeArchive: e.includeArchive,
|
||||
includeNested: e.includeNested,
|
||||
includeFiles: e.includeFiles,
|
||||
format: e.format,
|
||||
isJson: e.isJson,
|
||||
reqIds: e.reqIds,
|
||||
export: e.export,
|
||||
spaceId: e.spaceId,
|
||||
docs: e.docs,
|
||||
includeArchive: e.includeArchive,
|
||||
includeNested: e.includeNested,
|
||||
includeFiles: e.includeFiles,
|
||||
format: e.format,
|
||||
isJson: e.isJson,
|
||||
reqIds: e.reqIds,
|
||||
export: e.export,
|
||||
isLinkProcess: e.isLinkProcess,
|
||||
linkStateFilters: e.linkStateFilters,
|
||||
includeBackLinks: e.includeBackLinks,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *exportContext) getStateFilters(id string) *state.Filters {
|
||||
if doc, ok := e.docs[id]; ok && doc.isLink {
|
||||
return e.linkStateFilters
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *exportContext) exportObjects(ctx context.Context, queue process.Queue) (string, int, error) {
|
||||
var (
|
||||
err error
|
||||
|
@ -261,10 +292,11 @@ func (e *exportContext) exportDocs(ctx context.Context,
|
|||
succeed *int64,
|
||||
tasks []process.Task,
|
||||
) []process.Task {
|
||||
docsDetails := e.docs.transformToDetailsMap()
|
||||
for docId := range e.docs {
|
||||
did := docId
|
||||
task := func() {
|
||||
if werr := e.writeDoc(ctx, wr, did); werr != nil {
|
||||
if werr := e.writeDoc(ctx, wr, did, docsDetails); werr != nil {
|
||||
log.With("objectID", did).Warnf("can't export doc: %v", werr)
|
||||
} else {
|
||||
atomic.AddInt64(succeed, 1)
|
||||
|
@ -277,7 +309,7 @@ func (e *exportContext) exportDocs(ctx context.Context,
|
|||
|
||||
func (e *exportContext) exportGraphJson(ctx context.Context, succeed int, wr writer, queue process.Queue) int {
|
||||
mc := graphjson.NewMultiConverter(e.sbtProvider)
|
||||
mc.SetKnownDocs(e.docs)
|
||||
mc.SetKnownDocs(e.docs.transformToDetailsMap())
|
||||
var werr error
|
||||
if succeed, werr = e.writeMultiDoc(ctx, mc, wr, queue); werr != nil {
|
||||
log.Warnf("can't export docs: %v", werr)
|
||||
|
@ -291,7 +323,7 @@ func (e *exportContext) exportDotAndSVG(ctx context.Context, succeed int, wr wri
|
|||
format = dot.ExportFormatSVG
|
||||
}
|
||||
mc := dot.NewMultiConverter(format, e.sbtProvider)
|
||||
mc.SetKnownDocs(e.docs)
|
||||
mc.SetKnownDocs(e.docs.transformToDetailsMap())
|
||||
var werr error
|
||||
if succeed, werr = e.writeMultiDoc(ctx, mc, wr, queue); werr != nil {
|
||||
log.Warnf("can't export docs: %v", werr)
|
||||
|
@ -332,7 +364,7 @@ func (e *exportContext) getObjectsByIDs(isProtobuf bool) error {
|
|||
}
|
||||
for _, object := range res {
|
||||
id := object.Details.GetString(bundle.RelationKeyId)
|
||||
e.docs[id] = object.Details
|
||||
e.docs[id] = &Doc{Details: object.Details}
|
||||
}
|
||||
if isProtobuf {
|
||||
return e.processProtobuf()
|
||||
|
@ -385,7 +417,7 @@ func (e *exportContext) processNotProtobuf() error {
|
|||
}
|
||||
if e.includeNested {
|
||||
for _, id := range ids {
|
||||
e.addNestedObject(id, map[string]*domain.Details{})
|
||||
e.addNestedObject(id, map[string]*Doc{})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -426,7 +458,7 @@ func (e *exportContext) addDependentObjectsFromDataview() error {
|
|||
err error
|
||||
)
|
||||
for id, details := range e.docs {
|
||||
if isObjectWithDataview(details) {
|
||||
if isObjectWithDataview(details.Details) {
|
||||
viewDependentObjectsIds, err = e.getViewDependentObjects(id, viewDependentObjectsIds)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -443,14 +475,17 @@ func (e *exportContext) addDependentObjectsFromDataview() error {
|
|||
}
|
||||
for _, object := range append(viewDependentObjects, templates...) {
|
||||
id := object.Details.GetString(bundle.RelationKeyId)
|
||||
e.docs[id] = object.Details
|
||||
e.docs[id] = &Doc{
|
||||
Details: object.Details,
|
||||
isLink: e.isLinkProcess,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *exportContext) getViewDependentObjects(id string, viewDependentObjectsIds []string) ([]string, error) {
|
||||
err := cache.Do(e.picker, id, func(sb sb.SmartBlock) error {
|
||||
st := sb.NewState()
|
||||
st := sb.NewState().Copy().Filter(e.getStateFilters(id))
|
||||
viewDependentObjectsIds = append(viewDependentObjectsIds, objectlink.DependentObjectIDs(st, sb.Space(), objectlink.Flags{Blocks: true})...)
|
||||
return nil
|
||||
})
|
||||
|
@ -506,7 +541,7 @@ func (e *exportContext) addDerivedObjects() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (e *exportContext) getRelationsAndTypes(notProcessedObjects map[string]*domain.Details, processedObjects map[string]struct{}) ([]string, []string, []string, error) {
|
||||
func (e *exportContext) getRelationsAndTypes(notProcessedObjects map[string]*Doc, processedObjects map[string]struct{}) ([]string, []string, []string, error) {
|
||||
allRelations, allTypes, allSetOfList, err := e.collectDerivedObjects(notProcessedObjects)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
|
@ -525,11 +560,11 @@ func (e *exportContext) getRelationsAndTypes(notProcessedObjects map[string]*dom
|
|||
return allRelations, allTypes, allSetOfList, nil
|
||||
}
|
||||
|
||||
func (e *exportContext) collectDerivedObjects(objects map[string]*domain.Details) ([]string, []string, []string, error) {
|
||||
func (e *exportContext) collectDerivedObjects(objects map[string]*Doc) ([]string, []string, []string, error) {
|
||||
var relations, objectsTypes, setOf []string
|
||||
for id := range objects {
|
||||
err := cache.Do(e.picker, id, func(b sb.SmartBlock) error {
|
||||
state := b.NewState()
|
||||
state := b.NewState().Copy().Filter(e.getStateFilters(id))
|
||||
relations = lo.Union(relations, getObjectRelations(state))
|
||||
details := state.CombinedDetails()
|
||||
if isObjectWithDataview(details) {
|
||||
|
@ -539,8 +574,10 @@ func (e *exportContext) collectDerivedObjects(objects map[string]*domain.Details
|
|||
}
|
||||
relations = lo.Union(relations, dataviewRelations)
|
||||
}
|
||||
objectTypeId := details.GetString(bundle.RelationKeyType)
|
||||
objectsTypes = lo.Union(objectsTypes, []string{objectTypeId})
|
||||
if details.Has(bundle.RelationKeyType) {
|
||||
objectTypeId := details.GetString(bundle.RelationKeyType)
|
||||
objectsTypes = lo.Union(objectsTypes, []string{objectTypeId})
|
||||
}
|
||||
setOfList := details.GetStringList(bundle.RelationKeySetOf)
|
||||
setOf = lo.Union(setOf, setOfList)
|
||||
return nil
|
||||
|
@ -582,7 +619,7 @@ func getDataviewRelations(state *state.State) ([]string, error) {
|
|||
}
|
||||
|
||||
func (e *exportContext) getDerivedObjectsForTypes(allTypes []string, processedObjects map[string]struct{}) ([]string, []string, []string, error) {
|
||||
notProceedTypes := make(map[string]*domain.Details)
|
||||
notProceedTypes := make(map[string]*Doc)
|
||||
var relations, objectTypes []string
|
||||
for _, object := range allTypes {
|
||||
if _, ok := processedObjects[object]; ok {
|
||||
|
@ -609,12 +646,13 @@ func (e *exportContext) getTemplatesRelationsAndTypes(allTypes []string, process
|
|||
if len(templates) == 0 {
|
||||
return nil, nil, nil, nil
|
||||
}
|
||||
templatesToProcess := make(map[string]*domain.Details, len(templates))
|
||||
templatesToProcess := make(map[string]*Doc, len(templates))
|
||||
for _, template := range templates {
|
||||
id := template.Details.GetString(bundle.RelationKeyId)
|
||||
if _, ok := e.docs[id]; !ok {
|
||||
e.docs[id] = template.Details
|
||||
templatesToProcess[id] = template.Details
|
||||
templateDoc := &Doc{Details: template.Details, isLink: e.isLinkProcess}
|
||||
e.docs[id] = templateDoc
|
||||
templatesToProcess[id] = templateDoc
|
||||
}
|
||||
}
|
||||
templateRelations, templateType, templateSetOfList, err := e.getRelationsAndTypes(templatesToProcess, processedObjects)
|
||||
|
@ -671,7 +709,7 @@ func (e *exportContext) addRelation(relation database.Record) {
|
|||
relationKey := domain.RelationKey(relation.Details.GetString(bundle.RelationKeyRelationKey))
|
||||
if relationKey != "" && !bundle.HasRelation(relationKey) {
|
||||
id := relation.Details.GetString(bundle.RelationKeyId)
|
||||
e.docs[id] = relation.Details
|
||||
e.docs[id] = &Doc{Details: relation.Details, isLink: e.isLinkProcess}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -694,7 +732,7 @@ func (e *exportContext) addRelationOptions(relationKey string) error {
|
|||
}
|
||||
for _, option := range relationOptions {
|
||||
id := option.Details.GetString(bundle.RelationKeyId)
|
||||
e.docs[id] = option.Details
|
||||
e.docs[id] = &Doc{Details: option.Details, isLink: e.isLinkProcess}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -748,7 +786,7 @@ func (e *exportContext) addObjectsAndCollectRecommendedRelations(objectTypes []d
|
|||
return nil, err
|
||||
}
|
||||
id := objectTypes[i].Details.GetString(bundle.RelationKeyId)
|
||||
e.docs[id] = objectTypes[i].Details
|
||||
e.docs[id] = &Doc{Details: objectTypes[i].Details, isLink: e.isLinkProcess}
|
||||
if uniqueKey.SmartblockType() == smartblock.SmartBlockTypeObjectType {
|
||||
key, err := domain.GetTypeKeyFromRawUniqueKey(rawUniqueKey)
|
||||
if err != nil {
|
||||
|
@ -782,13 +820,13 @@ func (e *exportContext) addRecommendedRelations(recommendedRelations []string) e
|
|||
if bundle.IsSystemRelation(domain.RelationKey(uniqueKey.InternalKey())) {
|
||||
continue
|
||||
}
|
||||
e.docs[id] = relation.Details
|
||||
e.docs[id] = &Doc{Details: relation.Details, isLink: e.isLinkProcess}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *exportContext) addNestedObjects(ids []string) error {
|
||||
nestedDocs := make(map[string]*domain.Details, 0)
|
||||
nestedDocs := make(map[string]*Doc, 0)
|
||||
for _, id := range ids {
|
||||
e.addNestedObject(id, nestedDocs)
|
||||
}
|
||||
|
@ -798,6 +836,7 @@ func (e *exportContext) addNestedObjects(ids []string) error {
|
|||
exportCtxChild := e.copy()
|
||||
exportCtxChild.includeNested = false
|
||||
exportCtxChild.docs = nestedDocs
|
||||
exportCtxChild.isLinkProcess = true
|
||||
err := exportCtxChild.processProtobuf()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -810,10 +849,20 @@ func (e *exportContext) addNestedObjects(ids []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (e *exportContext) addNestedObject(id string, nestedDocs map[string]*domain.Details) {
|
||||
links, err := e.objectStore.SpaceIndex(e.spaceId).GetOutboundLinksById(id)
|
||||
func (e *exportContext) addNestedObject(id string, nestedDocs map[string]*Doc) {
|
||||
var links []string
|
||||
err := cache.Do(e.picker, id, func(sb sb.SmartBlock) error {
|
||||
st := sb.NewState().Copy().Filter(e.getStateFilters(id))
|
||||
links = objectlink.DependentObjectIDs(st, sb.Space(), objectlink.Flags{
|
||||
Blocks: true,
|
||||
Details: true,
|
||||
Collection: true,
|
||||
NoHiddenBundledRelations: true,
|
||||
NoBackLinks: !e.includeBackLinks,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("export failed to get outbound links for id: %s", err)
|
||||
return
|
||||
}
|
||||
for _, link := range links {
|
||||
|
@ -832,8 +881,9 @@ func (e *exportContext) addNestedObject(id string, nestedDocs map[string]*domain
|
|||
continue
|
||||
}
|
||||
if isLinkedObjectExist(rec) {
|
||||
nestedDocs[link] = rec[0].Details
|
||||
e.docs[link] = rec[0].Details
|
||||
exportDoc := &Doc{Details: rec[0].Details, isLink: true}
|
||||
nestedDocs[link] = exportDoc
|
||||
e.docs[link] = exportDoc
|
||||
e.addNestedObject(link, nestedDocs)
|
||||
}
|
||||
}
|
||||
|
@ -844,7 +894,7 @@ func (e *exportContext) fillLinkedFiles(id string) ([]string, error) {
|
|||
spaceIndex := e.objectStore.SpaceIndex(e.spaceId)
|
||||
var fileObjectsIds []string
|
||||
err := cache.Do(e.picker, id, func(b sb.SmartBlock) error {
|
||||
b.NewState().IterateLinkedFiles(func(fileObjectId string) {
|
||||
b.NewState().Copy().Filter(e.getStateFilters(id)).IterateLinkedFiles(func(fileObjectId string) {
|
||||
res, err := spaceIndex.Query(database.Query{
|
||||
Filters: []database.FilterRequest{
|
||||
{
|
||||
|
@ -861,7 +911,7 @@ func (e *exportContext) fillLinkedFiles(id string) ([]string, error) {
|
|||
if len(res) == 0 {
|
||||
return
|
||||
}
|
||||
e.docs[fileObjectId] = res[0].Details
|
||||
e.docs[fileObjectId] = &Doc{Details: res[0].Details, isLink: e.isLinkProcess}
|
||||
fileObjectsIds = append(fileObjectsIds, fileObjectId)
|
||||
})
|
||||
return nil
|
||||
|
@ -885,7 +935,7 @@ func (e *exportContext) getExistedObjects(isProtobuf bool) error {
|
|||
}
|
||||
res = append(res, archivedObjects...)
|
||||
}
|
||||
e.docs = make(map[string]*domain.Details, len(res))
|
||||
e.docs = make(map[string]*Doc, len(res))
|
||||
for _, info := range res {
|
||||
objectSpaceID := e.spaceId
|
||||
if objectSpaceID == "" {
|
||||
|
@ -899,15 +949,14 @@ func (e *exportContext) getExistedObjects(isProtobuf bool) error {
|
|||
if !objectValid(sbType, info, e.includeArchive, isProtobuf) {
|
||||
continue
|
||||
}
|
||||
e.docs[info.Id] = info.Details
|
||||
|
||||
e.docs[info.Id] = &Doc{Details: info.Details}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *exportContext) listTargetTypesFromTemplates(ids []string) []string {
|
||||
for id, object := range e.docs {
|
||||
if object.Has(bundle.RelationKeyTargetObjectType) {
|
||||
if object.Details.Has(bundle.RelationKeyTargetObjectType) {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
|
@ -950,13 +999,14 @@ func (e *exportContext) writeMultiDoc(ctx context.Context, mw converter.MultiCon
|
|||
return
|
||||
}
|
||||
|
||||
func (e *exportContext) writeDoc(ctx context.Context, wr writer, docId string) (err error) {
|
||||
func (e *exportContext) writeDoc(ctx context.Context, wr writer, docId string, details map[string]*domain.Details) (err error) {
|
||||
return cache.Do(e.picker, docId, func(b sb.SmartBlock) error {
|
||||
st := b.NewState()
|
||||
if st.CombinedDetails().GetBool(bundle.RelationKeyIsDeleted) {
|
||||
return nil
|
||||
}
|
||||
|
||||
st = st.Copy().Filter(e.getStateFilters(docId))
|
||||
if e.includeFiles && b.Type() == smartblock.SmartBlockTypeFileObject {
|
||||
fileName, err := e.saveFile(ctx, wr, b, e.spaceId == "")
|
||||
if err != nil {
|
||||
|
@ -978,7 +1028,7 @@ func (e *exportContext) writeDoc(ctx context.Context, wr writer, docId string) (
|
|||
case model.Export_JSON:
|
||||
conv = pbjson.NewConverter(st)
|
||||
}
|
||||
conv.SetKnownDocs(e.docs)
|
||||
conv.SetKnownDocs(details)
|
||||
result := conv.Convert(b.Type().ToProto())
|
||||
var filename string
|
||||
if e.format == model.Export_Markdown {
|
||||
|
@ -1198,7 +1248,7 @@ func cleanupFile(wr writer) {
|
|||
os.Remove(wr.Path())
|
||||
}
|
||||
|
||||
func listObjectIds(docs map[string]*domain.Details) []string {
|
||||
func listObjectIds(docs map[string]*Doc) []string {
|
||||
ids := make([]string, 0, len(docs))
|
||||
for id := range docs {
|
||||
ids = append(ids, id)
|
||||
|
@ -1209,3 +1259,21 @@ func listObjectIds(docs map[string]*domain.Details) []string {
|
|||
func isLinkedObjectExist(rec []database.Record) bool {
|
||||
return len(rec) > 0 && !rec[0].Details.GetBool(bundle.RelationKeyIsDeleted)
|
||||
}
|
||||
|
||||
func pbFiltersToState(filters *pb.RpcObjectListExportStateFilters) *state.Filters {
|
||||
if filters == nil {
|
||||
return nil
|
||||
}
|
||||
relationByLayoutList := state.RelationsByLayout{}
|
||||
for _, relationByLayout := range filters.RelationsWhiteList {
|
||||
allowedRelations := make([]domain.RelationKey, 0, len(relationByLayout.AllowedRelations))
|
||||
for _, relation := range relationByLayout.AllowedRelations {
|
||||
allowedRelations = append(allowedRelations, domain.RelationKey(relation))
|
||||
}
|
||||
relationByLayoutList[relationByLayout.Layout] = allowedRelations
|
||||
}
|
||||
return &state.Filters{
|
||||
RelationsWhiteList: relationByLayoutList,
|
||||
RemoveBlocks: filters.RemoveBlocks,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@ import (
|
|||
"archive/zip"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/gogo/protobuf/jsonpb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
|
@ -361,6 +363,453 @@ func TestExport_Export(t *testing.T) {
|
|||
assert.NotNil(t, err)
|
||||
assert.Equal(t, 0, success)
|
||||
})
|
||||
t.Run("export with filters success", func(t *testing.T) {
|
||||
// given
|
||||
storeFixture := objectstore.NewStoreFixture(t)
|
||||
objectTypeId := "objectTypeId"
|
||||
objectTypeUniqueKey, err := domain.NewUniqueKey(smartblock.SmartBlockTypeObjectType, objectTypeId)
|
||||
assert.Nil(t, err)
|
||||
objectId := "objectID"
|
||||
link := "linkId"
|
||||
|
||||
storeFixture.AddObjects(t, spaceId, []spaceindex.TestObject{
|
||||
{
|
||||
bundle.RelationKeyId: domain.String(link),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyDescription: domain.String("description"),
|
||||
bundle.RelationKeyLayout: domain.Int64(model.ObjectType_set),
|
||||
bundle.RelationKeyCamera: domain.String("test"),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: domain.String(objectId),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: domain.String(objectTypeId),
|
||||
bundle.RelationKeyUniqueKey: domain.String(objectTypeUniqueKey.Marshal()),
|
||||
bundle.RelationKeyLayout: domain.Int64(int64(model.ObjectType_objectType)),
|
||||
bundle.RelationKeyRecommendedRelations: domain.StringList([]string{addr.MissingObject}),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
},
|
||||
})
|
||||
|
||||
objectGetter := mock_cache.NewMockObjectGetterComponent(t)
|
||||
|
||||
smartBlockTest := smarttest.New(objectId)
|
||||
doc := smartBlockTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyId: domain.String(objectId),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
}))
|
||||
doc.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyType.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
})
|
||||
smartBlockTest.Doc = doc
|
||||
smartBlockTest.AddBlock(simple.New(&model.Block{Id: objectId, ChildrenIds: []string{"linkBlock"}, Content: &model.BlockContentOfSmartblock{Smartblock: &model.BlockContentSmartblock{}}}))
|
||||
smartBlockTest.AddBlock(simple.New(&model.Block{Id: "linkBlock", Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{TargetBlockId: link}}}))
|
||||
|
||||
objectType := smarttest.New(objectTypeId)
|
||||
objectTypeDoc := objectType.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyId: domain.String(objectTypeId),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
}))
|
||||
objectTypeDoc.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyType.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
})
|
||||
objectType.Doc = objectTypeDoc
|
||||
objectType.SetType(smartblock.SmartBlockTypeObjectType)
|
||||
|
||||
linkObject := smarttest.New(link)
|
||||
linkObjectDoc := linkObject.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyId: domain.String(link),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyDescription: domain.String("description"),
|
||||
bundle.RelationKeyLayout: domain.Int64(model.ObjectType_set),
|
||||
bundle.RelationKeyCamera: domain.String("test"),
|
||||
}))
|
||||
linkObjectDoc.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyType.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeySpaceId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyDescription.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyLayout.String(),
|
||||
Format: model.RelationFormat_number,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyCamera.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
})
|
||||
linkObject.Doc = linkObjectDoc
|
||||
linkObject.AddBlock(simple.New(&model.Block{Id: objectId, ChildrenIds: []string{"linkBlock"}, Content: &model.BlockContentOfSmartblock{Smartblock: &model.BlockContentSmartblock{}}}))
|
||||
linkObject.AddBlock(simple.New(&model.Block{Id: "linkBlock", Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{TargetBlockId: "link1"}}}))
|
||||
|
||||
objectGetter.EXPECT().GetObject(context.Background(), objectId).Return(smartBlockTest, nil).Times(4)
|
||||
objectGetter.EXPECT().GetObject(context.Background(), objectTypeId).Return(objectType, nil)
|
||||
objectGetter.EXPECT().GetObject(context.Background(), link).Return(linkObject, nil)
|
||||
|
||||
a := &app.App{}
|
||||
mockSender := mock_event.NewMockSender(t)
|
||||
mockSender.EXPECT().Broadcast(mock.Anything).Return()
|
||||
a.Register(testutil.PrepareMock(context.Background(), a, mockSender))
|
||||
service := process.New()
|
||||
err = service.Init(a)
|
||||
assert.Nil(t, err)
|
||||
|
||||
notifications := mock_notifications.NewMockNotifications(t)
|
||||
notificationSend := make(chan struct{})
|
||||
notifications.EXPECT().CreateAndSend(mock.Anything).RunAndReturn(func(notification *model.Notification) error {
|
||||
close(notificationSend)
|
||||
return nil
|
||||
})
|
||||
|
||||
provider := mock_typeprovider.NewMockSmartBlockTypeProvider(t)
|
||||
provider.EXPECT().Type(spaceId, link).Return(smartblock.SmartBlockTypePage, nil)
|
||||
|
||||
e := &export{
|
||||
objectStore: storeFixture,
|
||||
picker: objectGetter,
|
||||
processService: service,
|
||||
notificationService: notifications,
|
||||
sbtProvider: provider,
|
||||
}
|
||||
|
||||
// when
|
||||
path, success, err := e.Export(context.Background(), pb.RpcObjectListExportRequest{
|
||||
SpaceId: spaceId,
|
||||
Path: t.TempDir(),
|
||||
ObjectIds: []string{objectId},
|
||||
Format: model.Export_Protobuf,
|
||||
Zip: true,
|
||||
IncludeNested: true,
|
||||
IncludeFiles: true,
|
||||
IsJson: true,
|
||||
LinksStateFilters: &pb.RpcObjectListExportStateFilters{
|
||||
RelationsWhiteList: []*pb.RpcObjectListExportRelationsWhiteList{
|
||||
{
|
||||
Layout: model.ObjectType_set,
|
||||
AllowedRelations: []string{bundle.RelationKeyCamera.String()},
|
||||
},
|
||||
},
|
||||
RemoveBlocks: true,
|
||||
},
|
||||
})
|
||||
|
||||
// then
|
||||
<-notificationSend
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 3, success)
|
||||
|
||||
reader, err := zip.OpenReader(path)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Len(t, reader.File, 3)
|
||||
fileNames := make(map[string]bool, 3)
|
||||
for _, file := range reader.File {
|
||||
fileNames[file.Name] = true
|
||||
}
|
||||
|
||||
objectPath := filepath.Join(objectsDirectory, link+".pb.json")
|
||||
assert.True(t, fileNames[objectPath])
|
||||
|
||||
file, err := os.Open(objectPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var sn *pb.SnapshotWithType
|
||||
err = jsonpb.Unmarshal(file, sn)
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, sn.GetSnapshot().GetData().GetBlocks(), 1)
|
||||
assert.Equal(t, link, sn.GetSnapshot().GetData().GetBlocks()[0].GetId())
|
||||
assert.Len(t, sn.GetSnapshot().GetData().GetDetails().GetFields(), 1)
|
||||
assert.NotNil(t, sn.GetSnapshot().GetData().GetDetails().GetFields()[bundle.RelationKeyCamera.String()])
|
||||
assert.Len(t, sn.GetSnapshot().GetData().GetRelationLinks(), 1)
|
||||
assert.Equal(t, bundle.RelationKeyCamera.String(), sn.GetSnapshot().GetData().GetRelationLinks()[0].Key)
|
||||
})
|
||||
t.Run("export with backlinks", func(t *testing.T) {
|
||||
// given
|
||||
storeFixture := objectstore.NewStoreFixture(t)
|
||||
objectTypeId := "objectTypeId"
|
||||
objectTypeUniqueKey, err := domain.NewUniqueKey(smartblock.SmartBlockTypeObjectType, objectTypeId)
|
||||
assert.Nil(t, err)
|
||||
objectId := "objectID"
|
||||
link1 := "linkId"
|
||||
|
||||
storeFixture.AddObjects(t, spaceId, []spaceindex.TestObject{
|
||||
{
|
||||
bundle.RelationKeyId: domain.String(link1),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: domain.String(objectId),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyBacklinks: domain.StringList([]string{link1}),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: domain.String(objectTypeId),
|
||||
bundle.RelationKeyUniqueKey: domain.String(objectTypeUniqueKey.Marshal()),
|
||||
bundle.RelationKeyLayout: domain.Int64(int64(model.ObjectType_objectType)),
|
||||
bundle.RelationKeyRecommendedRelations: domain.StringList([]string{addr.MissingObject}),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
},
|
||||
})
|
||||
|
||||
objectGetter := mock_cache.NewMockObjectGetterComponent(t)
|
||||
|
||||
smartBlockTest := smarttest.New(objectId)
|
||||
doc := smartBlockTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyId: domain.String(objectId),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
bundle.RelationKeyBacklinks: domain.StringList([]string{link1}),
|
||||
}))
|
||||
doc.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyType.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyBacklinks.String(),
|
||||
Format: model.RelationFormat_object,
|
||||
})
|
||||
smartBlockTest.Doc = doc
|
||||
|
||||
objectType := smarttest.New(objectTypeId)
|
||||
objectTypeDoc := objectType.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyId: domain.String(objectTypeId),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
}))
|
||||
objectTypeDoc.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyType.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
})
|
||||
objectType.Doc = objectTypeDoc
|
||||
objectType.SetType(smartblock.SmartBlockTypeObjectType)
|
||||
|
||||
linkObject := smarttest.New(link1)
|
||||
linkObjectDoc := linkObject.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyId: domain.String(link1),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
}))
|
||||
linkObjectDoc.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyType.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeySpaceId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
})
|
||||
linkObject.Doc = linkObjectDoc
|
||||
|
||||
objectGetter.EXPECT().GetObject(context.Background(), objectId).Return(smartBlockTest, nil).Times(4)
|
||||
objectGetter.EXPECT().GetObject(context.Background(), objectTypeId).Return(objectType, nil)
|
||||
objectGetter.EXPECT().GetObject(context.Background(), link1).Return(linkObject, nil)
|
||||
|
||||
a := &app.App{}
|
||||
mockSender := mock_event.NewMockSender(t)
|
||||
mockSender.EXPECT().Broadcast(mock.Anything).Return()
|
||||
a.Register(testutil.PrepareMock(context.Background(), a, mockSender))
|
||||
service := process.New()
|
||||
err = service.Init(a)
|
||||
assert.Nil(t, err)
|
||||
|
||||
notifications := mock_notifications.NewMockNotifications(t)
|
||||
notificationSend := make(chan struct{})
|
||||
notifications.EXPECT().CreateAndSend(mock.Anything).RunAndReturn(func(notification *model.Notification) error {
|
||||
close(notificationSend)
|
||||
return nil
|
||||
})
|
||||
|
||||
provider := mock_typeprovider.NewMockSmartBlockTypeProvider(t)
|
||||
provider.EXPECT().Type(spaceId, link1).Return(smartblock.SmartBlockTypePage, nil)
|
||||
|
||||
e := &export{
|
||||
objectStore: storeFixture,
|
||||
picker: objectGetter,
|
||||
processService: service,
|
||||
notificationService: notifications,
|
||||
sbtProvider: provider,
|
||||
}
|
||||
|
||||
// when
|
||||
path, success, err := e.Export(context.Background(), pb.RpcObjectListExportRequest{
|
||||
SpaceId: spaceId,
|
||||
Path: t.TempDir(),
|
||||
ObjectIds: []string{objectId},
|
||||
Format: model.Export_Protobuf,
|
||||
Zip: true,
|
||||
IncludeNested: true,
|
||||
IncludeFiles: true,
|
||||
IsJson: true,
|
||||
IncludeBacklinks: true,
|
||||
})
|
||||
|
||||
// then
|
||||
<-notificationSend
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 3, success)
|
||||
|
||||
reader, err := zip.OpenReader(path)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Len(t, reader.File, 3)
|
||||
fileNames := make(map[string]bool, 3)
|
||||
for _, file := range reader.File {
|
||||
fileNames[file.Name] = true
|
||||
}
|
||||
|
||||
objectPath := filepath.Join(objectsDirectory, link1+".pb.json")
|
||||
assert.True(t, fileNames[objectPath])
|
||||
})
|
||||
t.Run("export without backlinks", func(t *testing.T) {
|
||||
// given
|
||||
storeFixture := objectstore.NewStoreFixture(t)
|
||||
objectTypeId := "objectTypeId"
|
||||
objectTypeUniqueKey, err := domain.NewUniqueKey(smartblock.SmartBlockTypeObjectType, objectTypeId)
|
||||
assert.Nil(t, err)
|
||||
objectId := "objectID"
|
||||
link1 := "linkId"
|
||||
|
||||
storeFixture.AddObjects(t, spaceId, []spaceindex.TestObject{
|
||||
{
|
||||
bundle.RelationKeyId: domain.String(link1),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: domain.String(objectId),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyBacklinks: domain.StringList([]string{link1}),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: domain.String(objectTypeId),
|
||||
bundle.RelationKeyUniqueKey: domain.String(objectTypeUniqueKey.Marshal()),
|
||||
bundle.RelationKeyLayout: domain.Int64(int64(model.ObjectType_objectType)),
|
||||
bundle.RelationKeyRecommendedRelations: domain.StringList([]string{addr.MissingObject}),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
},
|
||||
})
|
||||
|
||||
objectGetter := mock_cache.NewMockObjectGetterComponent(t)
|
||||
|
||||
smartBlockTest := smarttest.New(objectId)
|
||||
doc := smartBlockTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyId: domain.String(objectId),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
bundle.RelationKeyBacklinks: domain.StringList([]string{link1}),
|
||||
}))
|
||||
doc.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyType.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyBacklinks.String(),
|
||||
Format: model.RelationFormat_object,
|
||||
})
|
||||
smartBlockTest.Doc = doc
|
||||
|
||||
objectType := smarttest.New(objectTypeId)
|
||||
objectTypeDoc := objectType.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyId: domain.String(objectTypeId),
|
||||
bundle.RelationKeyType: domain.String(objectTypeId),
|
||||
}))
|
||||
objectTypeDoc.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyType.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
})
|
||||
objectType.Doc = objectTypeDoc
|
||||
objectType.SetType(smartblock.SmartBlockTypeObjectType)
|
||||
|
||||
objectGetter.EXPECT().GetObject(context.Background(), objectId).Return(smartBlockTest, nil).Times(4)
|
||||
objectGetter.EXPECT().GetObject(context.Background(), objectTypeId).Return(objectType, nil)
|
||||
|
||||
a := &app.App{}
|
||||
mockSender := mock_event.NewMockSender(t)
|
||||
mockSender.EXPECT().Broadcast(mock.Anything).Return()
|
||||
a.Register(testutil.PrepareMock(context.Background(), a, mockSender))
|
||||
service := process.New()
|
||||
err = service.Init(a)
|
||||
assert.Nil(t, err)
|
||||
|
||||
notifications := mock_notifications.NewMockNotifications(t)
|
||||
notificationSend := make(chan struct{})
|
||||
notifications.EXPECT().CreateAndSend(mock.Anything).RunAndReturn(func(notification *model.Notification) error {
|
||||
close(notificationSend)
|
||||
return nil
|
||||
})
|
||||
|
||||
provider := mock_typeprovider.NewMockSmartBlockTypeProvider(t)
|
||||
|
||||
e := &export{
|
||||
objectStore: storeFixture,
|
||||
picker: objectGetter,
|
||||
processService: service,
|
||||
notificationService: notifications,
|
||||
sbtProvider: provider,
|
||||
}
|
||||
|
||||
// when
|
||||
path, success, err := e.Export(context.Background(), pb.RpcObjectListExportRequest{
|
||||
SpaceId: spaceId,
|
||||
Path: t.TempDir(),
|
||||
ObjectIds: []string{objectId},
|
||||
Format: model.Export_Protobuf,
|
||||
Zip: true,
|
||||
IncludeNested: true,
|
||||
IncludeFiles: true,
|
||||
IsJson: true,
|
||||
IncludeBacklinks: false,
|
||||
})
|
||||
|
||||
// then
|
||||
<-notificationSend
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, success)
|
||||
|
||||
reader, err := zip.OpenReader(path)
|
||||
assert.Nil(t, err)
|
||||
|
||||
fileNames := make(map[string]bool, 2)
|
||||
for _, file := range reader.File {
|
||||
fileNames[file.Name] = true
|
||||
}
|
||||
|
||||
objectPath := filepath.Join(objectsDirectory, link1+".pb.json")
|
||||
assert.False(t, fileNames[objectPath])
|
||||
})
|
||||
}
|
||||
|
||||
func Test_docsForExport(t *testing.T) {
|
||||
|
@ -379,14 +828,49 @@ func Test_docsForExport(t *testing.T) {
|
|||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
},
|
||||
})
|
||||
err := storeFixture.SpaceIndex(spaceId).UpdateObjectLinks(context.Background(), "id", []string{"id1"})
|
||||
assert.Nil(t, err)
|
||||
|
||||
provider := mock_typeprovider.NewMockSmartBlockTypeProvider(t)
|
||||
provider.EXPECT().Type(spaceId, "id1").Return(smartblock.SmartBlockTypePage, nil)
|
||||
|
||||
objectGetter := mock_cache.NewMockObjectGetterComponent(t)
|
||||
smartBlockTest := smarttest.New("id")
|
||||
doc := smartBlockTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyId: domain.String("id"),
|
||||
bundle.RelationKeyType: domain.String("objectTypeId"),
|
||||
}))
|
||||
doc.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyType.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
})
|
||||
smartBlockTest.Doc = doc
|
||||
|
||||
smartBlockTest.AddBlock(simple.New(&model.Block{Id: "id", ChildrenIds: []string{"linkBlock"}, Content: &model.BlockContentOfSmartblock{Smartblock: &model.BlockContentSmartblock{}}}))
|
||||
smartBlockTest.AddBlock(simple.New(&model.Block{Id: "linkBlock", Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{TargetBlockId: "id1"}}}))
|
||||
|
||||
linkObject := smarttest.New("id1")
|
||||
linkObjectDoc := linkObject.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyId: domain.String("id1"),
|
||||
bundle.RelationKeyType: domain.String("objectTypeId"),
|
||||
}))
|
||||
linkObjectDoc.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyType.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
})
|
||||
linkObject.Doc = linkObjectDoc
|
||||
|
||||
objectGetter.EXPECT().GetObject(context.Background(), "id").Return(smartBlockTest, nil)
|
||||
objectGetter.EXPECT().GetObject(context.Background(), "id1").Return(linkObject, nil)
|
||||
|
||||
e := &export{
|
||||
objectStore: storeFixture,
|
||||
sbtProvider: provider,
|
||||
picker: objectGetter,
|
||||
}
|
||||
|
||||
expCtx := newExportContext(e, pb.RpcObjectListExportRequest{
|
||||
|
@ -396,7 +880,7 @@ func Test_docsForExport(t *testing.T) {
|
|||
})
|
||||
|
||||
// when
|
||||
err = expCtx.docsForExport()
|
||||
err := expCtx.docsForExport()
|
||||
|
||||
// then
|
||||
assert.Nil(t, err)
|
||||
|
@ -417,14 +901,32 @@ func Test_docsForExport(t *testing.T) {
|
|||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
},
|
||||
})
|
||||
err := storeFixture.SpaceIndex(spaceId).UpdateObjectLinks(context.Background(), "id", []string{"id1"})
|
||||
assert.Nil(t, err)
|
||||
objectGetter := mock_cache.NewMockObjectGetterComponent(t)
|
||||
smartBlockTest := smarttest.New("id")
|
||||
doc := smartBlockTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
bundle.RelationKeyId: domain.String("id"),
|
||||
bundle.RelationKeyType: domain.String("objectTypeId"),
|
||||
}))
|
||||
doc.AddRelationLinks(&model.RelationLink{
|
||||
Key: bundle.RelationKeyId.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
}, &model.RelationLink{
|
||||
Key: bundle.RelationKeyType.String(),
|
||||
Format: model.RelationFormat_longtext,
|
||||
})
|
||||
smartBlockTest.Doc = doc
|
||||
|
||||
smartBlockTest.AddBlock(simple.New(&model.Block{Id: "id", ChildrenIds: []string{"linkBlock"}, Content: &model.BlockContentOfSmartblock{Smartblock: &model.BlockContentSmartblock{}}}))
|
||||
smartBlockTest.AddBlock(simple.New(&model.Block{Id: "linkBlock", Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{TargetBlockId: "id1"}}}))
|
||||
|
||||
objectGetter.EXPECT().GetObject(context.Background(), "id").Return(smartBlockTest, nil)
|
||||
|
||||
provider := mock_typeprovider.NewMockSmartBlockTypeProvider(t)
|
||||
provider.EXPECT().Type(spaceId, "id1").Return(smartblock.SmartBlockTypePage, nil)
|
||||
e := &export{
|
||||
objectStore: storeFixture,
|
||||
sbtProvider: provider,
|
||||
picker: objectGetter,
|
||||
}
|
||||
expCtx := newExportContext(e, pb.RpcObjectListExportRequest{
|
||||
SpaceId: spaceId,
|
||||
|
@ -433,7 +935,7 @@ func Test_docsForExport(t *testing.T) {
|
|||
})
|
||||
|
||||
// when
|
||||
err = expCtx.docsForExport()
|
||||
err := expCtx.docsForExport()
|
||||
|
||||
// then
|
||||
assert.Nil(t, err)
|
||||
|
@ -770,10 +1272,10 @@ func Test_docsForExport(t *testing.T) {
|
|||
linkedObjectId := "linkedObjectId"
|
||||
storeFixture.AddObjects(t, spaceId, []objectstore.TestObject{
|
||||
{
|
||||
bundle.RelationKeyId: domain.String("id"),
|
||||
domain.RelationKey(relationKey): domain.String("test"),
|
||||
bundle.RelationKeyType: domain.String(objectTypeKey),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
bundle.RelationKeyId: domain.String("id"),
|
||||
relationKey: domain.String("test"),
|
||||
bundle.RelationKeyType: domain.String(objectTypeKey),
|
||||
bundle.RelationKeySpaceId: domain.String(spaceId),
|
||||
},
|
||||
{
|
||||
bundle.RelationKeyId: domain.String(relationKey),
|
||||
|
@ -810,9 +1312,6 @@ func Test_docsForExport(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
err = storeFixture.SpaceIndex(spaceId).UpdateObjectLinks(context.Background(), templateId, []string{linkedObjectId})
|
||||
assert.Nil(t, err)
|
||||
|
||||
objectGetter := mock_cache.NewMockObjectGetter(t)
|
||||
|
||||
template := smarttest.New(templateId)
|
||||
|
@ -828,6 +1327,8 @@ func Test_docsForExport(t *testing.T) {
|
|||
Format: model.RelationFormat_longtext,
|
||||
})
|
||||
template.Doc = templateDoc
|
||||
template.AddBlock(simple.New(&model.Block{Id: templateId, ChildrenIds: []string{"linkBlock"}, Content: &model.BlockContentOfSmartblock{Smartblock: &model.BlockContentSmartblock{}}}))
|
||||
template.AddBlock(simple.New(&model.Block{Id: "linkBlock", Content: &model.BlockContentOfLink{Link: &model.BlockContentLink{TargetBlockId: linkedObjectId}}}))
|
||||
|
||||
smartBlockTest := smarttest.New("id")
|
||||
doc := smartBlockTest.NewState().SetDetails(domain.NewDetailsFromMap(map[domain.RelationKey]domain.Value{
|
||||
|
|
|
@ -4,6 +4,8 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
|
@ -13,6 +15,7 @@ import (
|
|||
type Directory struct {
|
||||
fileReaders map[string]struct{}
|
||||
importPath string
|
||||
rootDirs map[string]bool
|
||||
}
|
||||
|
||||
func NewDirectory() *Directory {
|
||||
|
@ -23,6 +26,9 @@ func (d *Directory) Initialize(importPath string) error {
|
|||
files := make(map[string]struct{})
|
||||
err := filepath.Walk(importPath,
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
if strings.HasPrefix(info.Name(), ".DS_Store") {
|
||||
return nil
|
||||
}
|
||||
if info != nil && !info.IsDir() {
|
||||
files[path] = struct{}{}
|
||||
}
|
||||
|
@ -31,6 +37,7 @@ func (d *Directory) Initialize(importPath string) error {
|
|||
)
|
||||
d.fileReaders = files
|
||||
d.importPath = importPath
|
||||
d.rootDirs = findNonEmptyDirs(files)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -80,7 +87,44 @@ func (d *Directory) CountFilesWithGivenExtensions(extension []string) int {
|
|||
}
|
||||
|
||||
func (d *Directory) IsRootFile(fileName string) bool {
|
||||
return filepath.Dir(fileName) == d.importPath
|
||||
fileDir := filepath.Dir(fileName)
|
||||
return fileDir == d.importPath || d.rootDirs[fileDir]
|
||||
}
|
||||
|
||||
func (d *Directory) Close() {}
|
||||
|
||||
func findNonEmptyDirs(files map[string]struct{}) map[string]bool {
|
||||
dirs := make([]string, 0, len(files))
|
||||
for file := range files {
|
||||
dir := filepath.Dir(file)
|
||||
if dir == "." {
|
||||
return map[string]bool{dir: true}
|
||||
}
|
||||
dirs = append(dirs, dir)
|
||||
}
|
||||
sort.Strings(dirs)
|
||||
result := make(map[string]bool)
|
||||
visited := make(map[string]bool)
|
||||
|
||||
for _, dir := range dirs {
|
||||
if _, ok := visited[dir]; ok {
|
||||
continue
|
||||
}
|
||||
visited[dir] = true
|
||||
if isSubdirectoryOfAny(dir, result) {
|
||||
continue
|
||||
}
|
||||
result[dir] = true
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func isSubdirectoryOfAny(dir string, directories map[string]bool) bool {
|
||||
for base := range directories {
|
||||
if strings.HasPrefix(dir, base+string(filepath.Separator)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
102
core/block/import/common/source/directory_test.go
Normal file
102
core/block/import/common/source/directory_test.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package source
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func createTestDir(tempDir string, files map[string]string) error {
|
||||
for name, content := range files {
|
||||
fullPath := filepath.Join(tempDir, name)
|
||||
err := os.MkdirAll(filepath.Dir(fullPath), 0777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = file.Write([]byte(content))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDirectory_Initialize(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
// given
|
||||
files := map[string]string{
|
||||
"file1.txt": "test",
|
||||
filepath.Join("folder", "file2.txt"): "test",
|
||||
}
|
||||
tempDir := t.TempDir()
|
||||
err := createTestDir(tempDir, files)
|
||||
defer os.RemoveAll(tempDir)
|
||||
assert.NoError(t, err)
|
||||
// when
|
||||
directory := NewDirectory()
|
||||
err = directory.Initialize(tempDir)
|
||||
|
||||
// then
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tempDir, directory.importPath)
|
||||
assert.Len(t, directory.fileReaders, 2)
|
||||
expectedRoots := map[string]bool{tempDir: true}
|
||||
assert.Equal(t, expectedRoots, directory.rootDirs)
|
||||
})
|
||||
t.Run("directory with another dir inside", func(t *testing.T) {
|
||||
// given
|
||||
files := map[string]string{
|
||||
filepath.Join("folder", "file2.txt"): "test",
|
||||
filepath.Join("folder", "file3.txt"): "test",
|
||||
filepath.Join("folder", "folder1", "file4.txt"): "test",
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
err := createTestDir(tempDir, files)
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// when
|
||||
directory := NewDirectory()
|
||||
err = directory.Initialize(tempDir)
|
||||
|
||||
// then
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tempDir, directory.importPath)
|
||||
assert.Len(t, directory.fileReaders, 3)
|
||||
|
||||
expectedRoots := map[string]bool{filepath.Join(tempDir, "folder"): true}
|
||||
assert.Equal(t, expectedRoots, directory.rootDirs)
|
||||
})
|
||||
t.Run("directory with 2 dirs inside", func(t *testing.T) {
|
||||
// given
|
||||
files := map[string]string{
|
||||
filepath.Join("folder", "file2.txt"): "test",
|
||||
filepath.Join("folder1", "folder2", "file4.txt"): "test",
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
err := createTestDir(tempDir, files)
|
||||
assert.NoError(t, err)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// when
|
||||
directory := NewDirectory()
|
||||
err = directory.Initialize(tempDir)
|
||||
|
||||
// then
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tempDir, directory.importPath)
|
||||
assert.Len(t, directory.fileReaders, 2)
|
||||
|
||||
expectedRoots := map[string]bool{filepath.Join(tempDir, "folder"): true, filepath.Join(tempDir, "folder1", "folder2"): true}
|
||||
assert.Equal(t, expectedRoots, directory.rootDirs)
|
||||
})
|
||||
}
|
|
@ -20,6 +20,7 @@ type Zip struct {
|
|||
archiveReader *zip.ReadCloser
|
||||
fileReaders map[string]*zip.File
|
||||
originalToNormalizedNames map[string]string
|
||||
rootDirs map[string]bool
|
||||
}
|
||||
|
||||
func NewZip() *Zip {
|
||||
|
@ -33,16 +34,20 @@ func (z *Zip) Initialize(importPath string) error {
|
|||
return err
|
||||
}
|
||||
fileReaders := make(map[string]*zip.File, len(archiveReader.File))
|
||||
filePaths := make(map[string]struct{}, len(archiveReader.File))
|
||||
for i, f := range archiveReader.File {
|
||||
if strings.HasPrefix(f.Name, "__MACOSX/") {
|
||||
continue
|
||||
}
|
||||
normalizedName := normalizeName(f, i)
|
||||
fileReaders[normalizedName] = f
|
||||
filePaths[normalizedName] = struct{}{}
|
||||
if normalizedName != f.Name {
|
||||
z.originalToNormalizedNames[f.Name] = normalizedName
|
||||
}
|
||||
}
|
||||
|
||||
z.rootDirs = findNonEmptyDirs(filePaths)
|
||||
z.fileReaders = fileReaders
|
||||
return nil
|
||||
}
|
||||
|
@ -101,7 +106,8 @@ func (z *Zip) Close() {
|
|||
}
|
||||
|
||||
func (z *Zip) IsRootFile(fileName string) bool {
|
||||
return filepath.Dir(fileName) == "."
|
||||
fileDir := filepath.Dir(fileName)
|
||||
return fileDir == "." || z.rootDirs[fileDir]
|
||||
}
|
||||
|
||||
func (z *Zip) GetFileOriginalName(fileName string) string {
|
||||
|
|
119
core/block/import/common/source/zip_test.go
Normal file
119
core/block/import/common/source/zip_test.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package source
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func createTestZip(t *testing.T, files map[string]string) (string, error) {
|
||||
tmpFile, err := ioutil.TempFile(t.TempDir(), "test-*.zip")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(tmpFile)
|
||||
for name, content := range files {
|
||||
writer, err := zipWriter.Create(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = writer.Write([]byte(content))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if err := zipWriter.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tmpFile.Name(), nil
|
||||
}
|
||||
|
||||
func TestZip_Initialize(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
// given
|
||||
files := map[string]string{
|
||||
"file1.txt": "test",
|
||||
filepath.Join("folder", "file2.txt"): "test",
|
||||
}
|
||||
|
||||
zipPath, err := createTestZip(t, files)
|
||||
assert.NoError(t, err)
|
||||
defer os.Remove(zipPath)
|
||||
|
||||
// when
|
||||
zipInstance := NewZip()
|
||||
err = zipInstance.Initialize(zipPath)
|
||||
|
||||
// then
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, zipInstance.archiveReader)
|
||||
assert.Len(t, zipInstance.fileReaders, 2)
|
||||
|
||||
expectedRoots := map[string]bool{".": true}
|
||||
assert.Equal(t, expectedRoots, zipInstance.rootDirs)
|
||||
})
|
||||
t.Run("zip files with dir inside", func(t *testing.T) {
|
||||
// given
|
||||
files := map[string]string{
|
||||
filepath.Join("folder", "file2.txt"): "test",
|
||||
filepath.Join("folder", "file3.txt"): "test",
|
||||
filepath.Join("folder", "folder1", "file4.txt"): "test",
|
||||
}
|
||||
|
||||
zipPath, err := createTestZip(t, files)
|
||||
assert.NoError(t, err)
|
||||
defer os.Remove(zipPath)
|
||||
|
||||
// when
|
||||
zipInstance := NewZip()
|
||||
err = zipInstance.Initialize(zipPath)
|
||||
|
||||
// then
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, zipInstance.archiveReader)
|
||||
assert.Len(t, zipInstance.fileReaders, 3)
|
||||
|
||||
expectedRoots := map[string]bool{"folder": true}
|
||||
assert.Equal(t, expectedRoots, zipInstance.rootDirs)
|
||||
})
|
||||
t.Run("zip files with 2 dirs inside", func(t *testing.T) {
|
||||
// given
|
||||
files := map[string]string{
|
||||
filepath.Join("folder", "file2.txt"): "test",
|
||||
filepath.Join("folder1", "folder2", "file4.txt"): "test",
|
||||
}
|
||||
|
||||
zipPath, err := createTestZip(t, files)
|
||||
assert.NoError(t, err)
|
||||
defer os.Remove(zipPath)
|
||||
|
||||
// when
|
||||
zipInstance := NewZip()
|
||||
err = zipInstance.Initialize(zipPath)
|
||||
|
||||
// then
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, zipInstance.archiveReader)
|
||||
assert.Len(t, zipInstance.fileReaders, 2)
|
||||
|
||||
expectedRoots := map[string]bool{"folder": true, filepath.Join("folder1", "folder2"): true}
|
||||
assert.Equal(t, expectedRoots, zipInstance.rootDirs)
|
||||
})
|
||||
t.Run("invalid path", func(t *testing.T) {
|
||||
// given
|
||||
zipInstance := NewZip()
|
||||
|
||||
// when
|
||||
err := zipInstance.Initialize("invalid_path.zip")
|
||||
|
||||
// then
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
|
@ -285,7 +285,7 @@ func (p *Pb) getSnapshotFromFile(rd io.ReadCloser, name string) (*common.Snapsho
|
|||
defer rd.Close()
|
||||
if filepath.Ext(name) == ".json" {
|
||||
snapshot := &pb.SnapshotWithType{}
|
||||
um := jsonpb.Unmarshaler{}
|
||||
um := jsonpb.Unmarshaler{AllowUnknownFields: true}
|
||||
if uErr := um.Unmarshal(rd, snapshot); uErr != nil {
|
||||
return nil, fmt.Errorf("PB:GetSnapshot %w", uErr)
|
||||
}
|
||||
|
|
|
@ -4,9 +4,8 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/anyproto/any-sync/commonspace/spacestorage"
|
||||
|
||||
"github.com/anyproto/anytype-heart/space/spacecore/storage"
|
||||
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
|
||||
)
|
||||
|
||||
const CName = "block.object.resolver"
|
||||
|
@ -21,12 +20,12 @@ func New() Resolver {
|
|||
}
|
||||
|
||||
type resolver struct {
|
||||
storage storage.ClientStorage
|
||||
objectStore objectstore.ObjectStore
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (r *resolver) Init(a *app.App) (err error) {
|
||||
r.storage = a.MustComponent(spacestorage.CName).(storage.ClientStorage)
|
||||
r.objectStore = a.MustComponent(objectstore.CName).(objectstore.ObjectStore)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -35,5 +34,5 @@ func (r *resolver) Name() (name string) {
|
|||
}
|
||||
|
||||
func (r *resolver) ResolveSpaceID(objectID string) (string, error) {
|
||||
return r.storage.GetSpaceID(objectID)
|
||||
return r.objectStore.GetSpaceId(objectID)
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ type Cache interface {
|
|||
GetObjectWithTimeout(ctx context.Context, id string) (sb smartblock.SmartBlock, err error)
|
||||
DoLockedIfNotExists(objectID string, proc func() error) error
|
||||
Remove(ctx context.Context, objectID string) error
|
||||
TryRemove(objectId string) (bool, error)
|
||||
CloseBlocks()
|
||||
|
||||
Close(ctx context.Context) error
|
||||
|
@ -155,6 +156,10 @@ func (c *objectCache) Remove(ctx context.Context, objectID string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (c *objectCache) TryRemove(objectId string) (bool, error) {
|
||||
return c.cache.TryRemove(objectId)
|
||||
}
|
||||
|
||||
func (c *objectCache) GetObjectWithTimeout(ctx context.Context, id string) (sb smartblock.SmartBlock, err error) {
|
||||
if _, ok := ctx.Deadline(); !ok {
|
||||
var cancel context.CancelFunc
|
||||
|
|
|
@ -785,6 +785,62 @@ func (_c *MockCache_Remove_Call) RunAndReturn(run func(context.Context, string)
|
|||
return _c
|
||||
}
|
||||
|
||||
// TryRemove provides a mock function with given fields: objectId
|
||||
func (_m *MockCache) TryRemove(objectId string) (bool, error) {
|
||||
ret := _m.Called(objectId)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for TryRemove")
|
||||
}
|
||||
|
||||
var r0 bool
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (bool, error)); ok {
|
||||
return rf(objectId)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) bool); ok {
|
||||
r0 = rf(objectId)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(objectId)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockCache_TryRemove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'TryRemove'
|
||||
type MockCache_TryRemove_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// TryRemove is a helper method to define mock.On call
|
||||
// - objectId string
|
||||
func (_e *MockCache_Expecter) TryRemove(objectId interface{}) *MockCache_TryRemove_Call {
|
||||
return &MockCache_TryRemove_Call{Call: _e.mock.On("TryRemove", objectId)}
|
||||
}
|
||||
|
||||
func (_c *MockCache_TryRemove_Call) Run(run func(objectId string)) *MockCache_TryRemove_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCache_TryRemove_Call) Return(_a0 bool, _a1 error) *MockCache_TryRemove_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockCache_TryRemove_Call) RunAndReturn(run func(string) (bool, error)) *MockCache_TryRemove_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockCache creates a new instance of MockCache. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockCache(t interface {
|
||||
|
|
|
@ -20,7 +20,7 @@ func Test_Payloads(t *testing.T) {
|
|||
changePayload := []byte("some")
|
||||
keys, err := accountdata.NewRandom()
|
||||
require.NoError(t, err)
|
||||
aclList, err := list.NewTestDerivedAcl("spaceId", keys)
|
||||
aclList, err := list.NewInMemoryDerivedAcl("spaceId", keys)
|
||||
require.NoError(t, err)
|
||||
timestamp := time.Now().Add(time.Hour).Unix()
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/anyproto/any-sync/app"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/lastused"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/state"
|
||||
"github.com/anyproto/anytype-heart/core/block/restriction"
|
||||
"github.com/anyproto/anytype-heart/core/block/source"
|
||||
|
@ -65,7 +64,6 @@ type service struct {
|
|||
bookmarkService bookmarkService
|
||||
spaceService space.Service
|
||||
templateService templateService
|
||||
lastUsedUpdater lastused.ObjectUsageUpdater
|
||||
archiver objectArchiver
|
||||
}
|
||||
|
||||
|
@ -79,7 +77,6 @@ func (s *service) Init(a *app.App) (err error) {
|
|||
s.collectionService = app.MustComponent[collectionService](a)
|
||||
s.spaceService = app.MustComponent[space.Service](a)
|
||||
s.templateService = app.MustComponent[templateService](a)
|
||||
s.lastUsedUpdater = app.MustComponent[lastused.ObjectUsageUpdater](a)
|
||||
s.archiver = app.MustComponent[objectArchiver](a)
|
||||
return nil
|
||||
}
|
||||
|
@ -211,3 +208,9 @@ func buildDateObject(space clientspace.Space, details *domain.Details) (string,
|
|||
details, err = detailsGetter.DetailsFromId()
|
||||
return dateObject.Id(), details, err
|
||||
}
|
||||
|
||||
func setOriginalCreatedTimestamp(state *state.State, details *domain.Details) {
|
||||
if createDate := details.GetInt64(bundle.RelationKeyCreatedDate); createDate != 0 {
|
||||
state.SetOriginalCreatedTimestamp(createDate)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/lastused/mock_lastused"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/smartblock/smarttest"
|
||||
"github.com/anyproto/anytype-heart/core/block/editor/state"
|
||||
"github.com/anyproto/anytype-heart/core/domain"
|
||||
|
@ -26,7 +25,6 @@ type fixture struct {
|
|||
spaceService *mock_space.MockService
|
||||
spc *mock_clientspace.MockSpace
|
||||
templateService *testTemplateService
|
||||
lastUsedService *mock_lastused.MockObjectUsageUpdater
|
||||
service Service
|
||||
}
|
||||
|
||||
|
@ -35,19 +33,16 @@ func newFixture(t *testing.T) *fixture {
|
|||
spc := mock_clientspace.NewMockSpace(t)
|
||||
|
||||
templateSvc := &testTemplateService{}
|
||||
lastUsedSvc := mock_lastused.NewMockObjectUsageUpdater(t)
|
||||
|
||||
s := &service{
|
||||
spaceService: spaceService,
|
||||
templateService: templateSvc,
|
||||
lastUsedUpdater: lastUsedSvc,
|
||||
}
|
||||
|
||||
return &fixture{
|
||||
spaceService: spaceService,
|
||||
spc: spc,
|
||||
templateService: templateSvc,
|
||||
lastUsedService: lastUsedSvc,
|
||||
service: s,
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +72,6 @@ func TestService_CreateObject(t *testing.T) {
|
|||
f.spaceService.EXPECT().Get(mock.Anything, mock.Anything).Return(f.spc, nil)
|
||||
f.spc.EXPECT().CreateTreeObject(mock.Anything, mock.Anything).Return(sb, nil)
|
||||
f.spc.EXPECT().Id().Return(spaceId)
|
||||
f.lastUsedService.EXPECT().UpdateLastUsedDate(spaceId, bundle.TypeKeyTemplate, mock.Anything).Return()
|
||||
|
||||
// when
|
||||
id, _, err := f.service.CreateObject(context.Background(), spaceId, CreateObjectRequest{
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
|
||||
"go.uber.org/zap"
|
||||
|
@ -213,6 +214,7 @@ func (s *service) reinstallObject(
|
|||
st.SetDetails(installingDetails)
|
||||
st.SetDetailAndBundledRelation(bundle.RelationKeyIsUninstalled, domain.Bool(false))
|
||||
st.SetDetailAndBundledRelation(bundle.RelationKeyIsDeleted, domain.Bool(false))
|
||||
st.SetOriginalCreatedTimestamp(time.Now().Unix())
|
||||
|
||||
key = domain.TypeKey(st.UniqueKeyInternal())
|
||||
details = st.CombinedDetails()
|
||||
|
@ -253,9 +255,7 @@ func (s *service) prepareDetailsForInstallingObject(
|
|||
details.SetString(bundle.RelationKeySpaceId, spaceID)
|
||||
details.SetString(bundle.RelationKeySourceObject, sourceId)
|
||||
details.SetBool(bundle.RelationKeyIsReadonly, false)
|
||||
|
||||
// we should delete old createdDate as it belongs to source object from marketplace
|
||||
details.Delete(bundle.RelationKeyCreatedDate)
|
||||
details.SetInt64(bundle.RelationKeyCreatedDate, time.Now().Unix())
|
||||
|
||||
if isNewSpace {
|
||||
lastused.SetLastUsedDateForInitialObjectType(sourceId, details)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue