├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ ├── azure-static-web-apps-calm-moss-08cfe3010.yml │ ├── azure-static-web-apps-icy-sand-097a6230f.yml │ ├── azure-static-web-apps-red-river-0f622590f.yml │ ├── data.yml │ ├── lint.yml │ ├── lock.yml │ └── main.yaml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── blox.cue ├── cmd └── blox │ └── main.go ├── config.go ├── config_test.go ├── content ├── graphql.go ├── postplugins.go ├── preplugins.go ├── render.go ├── repository.go └── rest.go ├── dogfood ├── .gitignore ├── blox.cue ├── data │ ├── articles │ │ ├── another.md │ │ └── hello-world.md │ ├── categories │ │ └── introduction.md │ ├── images │ │ ├── jpg │ │ │ └── main-img-preview.yaml │ │ └── png │ │ │ └── bketelsens-report.yaml │ ├── pages │ │ ├── about.md │ │ ├── getting-started.md │ │ ├── index.md │ │ ├── introduction.md │ │ └── managing-content.md │ ├── profiles │ │ └── bketelsen.md │ ├── sections │ │ ├── getting-started.md │ │ ├── introduction.md │ │ └── manage-content.md │ ├── tpl │ │ ├── article.txt.tmpl │ │ ├── articles.txt.tmpl │ │ └── brian.txt.tmpl │ └── websites │ │ └── www.yaml ├── repository.cue ├── repository │ ├── _build │ │ ├── index.html │ │ └── manifest.json │ ├── article │ │ └── v1 │ │ │ └── schema.cue │ ├── category │ │ └── v1 │ │ │ └── schema.cue │ ├── page │ │ └── v1 │ │ │ └── schema.cue │ ├── profile │ │ └── v1 │ │ │ └── schema.cue │ ├── section │ │ └── v1 │ │ │ └── schema.cue │ └── website │ │ └── v1 │ │ └── schema.cue ├── schemata │ ├── article_v1.cue │ ├── category_v1.cue │ ├── image_v1.cue │ ├── page_v1.cue │ ├── profile_v1.cue │ ├── section_v1.cue │ └── website_v1.cue ├── sites │ ├── api.cueblox.com │ │ ├── api │ │ │ ├── .funcignore │ │ │ ├── .gitignore │ │ │ ├── .vscode │ │ │ │ └── extensions.json │ │ │ ├── DataApi │ │ │ │ ├── function.json │ │ │ │ ├── index.js │ │ │ │ └── sample.dat │ │ │ ├── GraphQL │ │ │ │ ├── function.json │ │ │ │ └── index.js │ │ │ ├── host.json │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── proxies.json │ │ └── app │ │ │ ├── index.html │ │ │ └── styles.css │ ├── docs │ │ ├── .gitignore │ │ ├── README.md │ │ ├── docs │ │ │ ├── assets │ │ │ │ └── cueblox-logo.svg │ │ │ ├── cmd │ │ │ │ ├── blox.md │ │ │ │ ├── blox_build.md │ │ │ │ ├── blox_completion.md │ │ │ │ ├── blox_init.md │ │ │ │ ├── blox_new.md │ │ │ │ ├── blox_remote.md │ │ │ │ ├── blox_remote_get.md │ │ │ │ ├── blox_remote_list.md │ │ │ │ ├── blox_render.md │ │ │ │ ├── blox_repo.md │ │ │ │ ├── blox_repo_build.md │ │ │ │ ├── blox_repo_init.md │ │ │ │ ├── blox_schema.md │ │ │ │ ├── blox_schema_list.md │ │ │ │ ├── blox_schema_new.md │ │ │ │ ├── blox_schema_version.md │ │ │ │ ├── blox_schema_version_add.md │ │ │ │ └── blox_serve.md │ │ │ ├── conversion.md │ │ │ ├── creating-your-blox │ │ │ │ ├── first-data.md │ │ │ │ ├── first-schema.md │ │ │ │ └── init.md │ │ │ ├── custom_schema.md │ │ │ ├── extras.md │ │ │ ├── foundations.md │ │ │ ├── get-started.md │ │ │ ├── img │ │ │ │ └── pyramid.png │ │ │ ├── index.md │ │ │ ├── introduction.md │ │ │ ├── recipes │ │ │ │ ├── github-releases.md │ │ │ │ ├── graphql.md │ │ │ │ ├── index.md │ │ │ │ ├── monorepo.md │ │ │ │ └── rest.md │ │ │ ├── shared_schema.md │ │ │ └── validation.md │ │ ├── mkdocs.yml │ │ ├── poetry.lock │ │ └── pyproject.toml │ └── netlify │ │ ├── api │ │ └── index.mjs │ │ ├── index.html │ │ ├── netlify.toml │ │ ├── package.json │ │ └── styles.css └── static │ └── images │ ├── jpg │ └── main-img-preview.jpg │ ├── png │ └── bketelsens-report.png │ └── svg │ └── mermaid-diagram-20210313160142.svg ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── internal ├── cmd │ ├── blox.cue │ ├── blox_build.go │ ├── blox_init.go │ ├── blox_new.go │ ├── blox_render.go │ ├── completion.go │ ├── docs.go │ ├── remote.go │ ├── remote_get.go │ ├── remote_list.go │ ├── repo.go │ ├── repo_build.go │ ├── repo_init.go │ ├── root.go │ ├── schema.go │ ├── schema_list.go │ ├── schema_new.go │ ├── schema_validation.cue │ ├── schema_version.go │ ├── schema_version_add.go │ └── serve.go ├── cuedb │ ├── config.cue │ ├── dataset.go │ ├── engine.go │ ├── engine_test.go │ ├── graphql.go │ └── graphql_test.go ├── cueutils │ ├── utils.go │ └── utils_test.go ├── encoding │ └── markdown │ │ ├── markdown.go │ │ └── markdown_test.go ├── hosting │ └── helpers.go └── repository │ ├── blox.go │ ├── config.cue │ ├── repository.go │ ├── schema.cue │ ├── schema.go │ └── version.go ├── pkgs └── blox │ └── default.nix ├── plugins ├── postbuild_interface.go ├── prebuild_interface.go └── shared │ ├── post.go │ └── pre.go ├── result ├── runtime.go └── scripts ├── cmd_docs.sh └── completions.sh /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.224.2/containers/go/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.16, 1.17, 1-bullseye, 1.16-bullseye, 1.17-bullseye, 1-buster, 1.16-buster, 1.17-buster 4 | ARG VARIANT="1.17-bullseye" 5 | FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT} 6 | 7 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 8 | ARG NODE_VERSION="none" 9 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 10 | 11 | # [Optional] Uncomment this section to install additional OS packages. 12 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 13 | # && apt-get -y install --no-install-recommends 14 | 15 | # [Optional] Uncomment the next lines to use go get to install anything else you need 16 | # USER vscode 17 | # RUN go get -x 18 | 19 | # [Optional] Uncomment this line to install global node packages. 20 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 21 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.224.2/containers/go 3 | { 4 | "name": "Go", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Update the VARIANT arg to pick a version of Go: 1, 1.16, 1.17 9 | // Append -bullseye or -buster to pin to an OS version. 10 | // Use -bullseye variants on local arm64/Apple Silicon. 11 | "VARIANT": "1-bullseye", 12 | // Options 13 | "NODE_VERSION": "lts/*" 14 | } 15 | }, 16 | "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], 17 | 18 | // Set *default* container specific settings.json values on container create. 19 | "settings": { 20 | "go.toolsManagement.checkForUpdates": "local", 21 | "go.useLanguageServer": true, 22 | "go.gopath": "/go", 23 | "go.goroot": "/usr/local/go" 24 | }, 25 | 26 | // Add the IDs of extensions you want installed when the container is created. 27 | "extensions": [ 28 | "golang.Go" 29 | ], 30 | 31 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 32 | // "forwardPorts": [], 33 | 34 | // Use 'postCreateCommand' to run commands after the container is created. 35 | // "postCreateCommand": "go version", 36 | 37 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 38 | "remoteUser": "vscode" 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-calm-moss-08cfe3010.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs Web Site 2 | 3 | on: 4 | push: 5 | paths: 6 | - .github/** 7 | - dogfood/** 8 | 9 | jobs: 10 | build_and_deploy_job: 11 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 12 | runs-on: ubuntu-latest 13 | name: Build and Deploy Job 14 | steps: 15 | - uses: actions/checkout@v2 16 | with: 17 | submodules: true 18 | - uses: actions/setup-python@v2 19 | with: 20 | python-version: 3.x 21 | - name: Install mkdocs 22 | run: pip install mkdocs-material 23 | working-directory: dogfood/sites/docs 24 | - name: Build mkdocs 25 | run: mkdocs build 26 | working-directory: dogfood/sites/docs 27 | 28 | - name: Build And Deploy 29 | id: builddeploy 30 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 31 | with: 32 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_CALM_MOSS_08CFE3010 }} 33 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 34 | action: "upload" 35 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 36 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 37 | app_location: "/dogfood/sites/docs/site" # App source code path 38 | api_location: "" # Api source code path - optional 39 | ###### End of Repository/Build Configurations ###### 40 | 41 | close_pull_request_job: 42 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 43 | runs-on: ubuntu-latest 44 | name: Close Pull Request Job 45 | steps: 46 | - name: Close Pull Request 47 | id: closepullrequest 48 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 49 | with: 50 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_CALM_MOSS_08CFE3010 }} 51 | action: "close" 52 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-icy-sand-097a6230f.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Repository Server 2 | 3 | on: 4 | push: 5 | paths: 6 | - .github/** 7 | - dogfood/** 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build_and_deploy_job: 13 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 14 | runs-on: ubuntu-latest 15 | name: Build and Deploy Job 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | submodules: true 20 | - name: Build And Deploy 21 | id: builddeploy 22 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 23 | with: 24 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ICY_SAND_097A6230F }} 25 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 26 | action: "upload" 27 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 28 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 29 | app_location: "/dogfood/repository/_build" # App source code path 30 | api_location: "api" # Api source code path - optional 31 | output_location: "" # Built app content directory - optional 32 | ###### End of Repository/Build Configurations ###### 33 | 34 | close_pull_request_job: 35 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 36 | runs-on: ubuntu-latest 37 | name: Close Pull Request Job 38 | steps: 39 | - name: Close Pull Request 40 | id: closepullrequest 41 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 42 | with: 43 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_ICY_SAND_097A6230F }} 44 | action: "close" 45 | -------------------------------------------------------------------------------- /.github/workflows/azure-static-web-apps-red-river-0f622590f.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Content Server 2 | 3 | on: 4 | push: 5 | paths: 6 | - .github/** 7 | - dogfood/** 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build_and_deploy_job: 13 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 14 | runs-on: ubuntu-latest 15 | name: Build and Deploy Job 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | submodules: true 20 | - name: Build And Deploy 21 | id: builddeploy 22 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 23 | with: 24 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_RED_RIVER_0F622590F }} 25 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 26 | action: "upload" 27 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 28 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 29 | app_location: "dogfood/sites/api.cueblox.com/app" # App source code path 30 | api_location: "dogfood/sites/api.cueblox.com/api" # Api source code path - optional 31 | output_location: "" # Built app content directory - optional 32 | ###### End of Repository/Build Configurations ###### 33 | 34 | close_pull_request_job: 35 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 36 | runs-on: ubuntu-latest 37 | name: Close Pull Request Job 38 | steps: 39 | - name: Close Pull Request 40 | id: closepullrequest 41 | uses: Azure/static-web-apps-deploy@v0.0.1-preview 42 | with: 43 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_RED_RIVER_0F622590F }} 44 | action: "close" 45 | -------------------------------------------------------------------------------- /.github/workflows/data.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release Data 2 | on: 3 | push: 4 | paths: 5 | - .github/** 6 | - dogfood/data/** 7 | - dogfood/schemata/** 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | name: Publish CueBlox 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Build & Validate Blox Data 17 | id: build 18 | uses: cueblox/github-action@v0.0.9 19 | with: 20 | directory: dogfood 21 | 22 | - uses: marvinpinto/action-automatic-releases@latest 23 | with: 24 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 25 | automatic_release_tag: "blox" 26 | prerelease: true 27 | title: "CueBlox" 28 | files: | 29 | dogfood/_build/data.json 30 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/setup-go@v2 15 | with: 16 | go-version: ~1.16 17 | - uses: actions/checkout@v2 18 | - name: golangci-lint 19 | uses: golangci/golangci-lint-action@v2 20 | with: 21 | skip-go-installation: true 22 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: lock-inactive 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | schedule: 8 | - cron: '0 * * * *' 9 | 10 | jobs: 11 | lock: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: dessant/lock-threads@v2 15 | with: 16 | github-token: ${{ github.token }} 17 | issue-lock-inactive-days: 30 18 | pr-lock-inactive-days: 30 19 | issue-lock-comment: > 20 | This issue has been automatically locked since there 21 | has not been any recent activity after it was closed. 22 | Please open a new issue for related bugs. 23 | pr-lock-comment: > 24 | This pull request has been automatically locked since there 25 | has not been any recent activity after it was closed. 26 | Please open a new issue for related bugs. 27 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | jobs: 12 | unit-tests: 13 | strategy: 14 | matrix: 15 | go-version: [ 1.16 ] 16 | os: [ ubuntu-latest, macos-latest, windows-latest ] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - 20 | name: Checkout 21 | uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 0 24 | - 25 | name: Set up Go 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: ${{ matrix.go-version }} 29 | - 30 | name: Cache Go modules 31 | uses: actions/cache@v1 32 | with: 33 | path: ~/go/pkg/mod 34 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 35 | restore-keys: | 36 | ${{ runner.os }}-go- 37 | - 38 | name: Make Unit Tests 39 | run: make test 40 | - 41 | name: Diff 42 | run: git diff 43 | - 44 | name: Upload coverage 45 | uses: codecov/codecov-action@v1 46 | if: matrix.os == 'ubuntu-latest' 47 | with: 48 | token: ${{ secrets.CODECOV_TOKEN }} 49 | file: ./coverage.txt 50 | goreleaser: 51 | strategy: 52 | matrix: 53 | go-version: [ 1.16 ] 54 | runs-on: ubuntu-latest 55 | if: startsWith(github.ref, 'refs/tags/') 56 | needs: 57 | - unit-tests 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v2 61 | with: 62 | fetch-depth: 0 63 | 64 | - name: Set up QEMU 65 | uses: docker/setup-qemu-action@v1 66 | 67 | - name: Set up Docker Buildx 68 | uses: docker/setup-buildx-action@v1 69 | 70 | - name: Login to Container Registry 71 | uses: docker/login-action@v1 72 | with: 73 | registry: ghcr.io 74 | username: ${{ github.repository_owner }} 75 | password: ${{ secrets.GITHUB_TOKEN }} 76 | 77 | - name: Set up Go 78 | uses: actions/setup-go@v2 79 | with: 80 | go-version: ${{ matrix.go-version }} 81 | - name: Cache Go modules 82 | uses: actions/cache@v1 83 | with: 84 | path: ~/go/pkg/mod 85 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 86 | restore-keys: | 87 | ${{ runner.os }}-go- 88 | - name: Setup 89 | run: make setup 90 | - name: Make build 91 | run: make build 92 | 93 | - name: Run GoReleaser 94 | uses: goreleaser/goreleaser-action@v2 95 | if: success() 96 | with: 97 | version: latest 98 | args: release --rm-dist 99 | env: 100 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 101 | TAP_WRITE_KEY: ${{ secrets.TAP_WRITE_KEY }} 102 | - name: Clear 103 | run: | 104 | rm -f ${HOME}/.docker/config.json 105 | 106 | 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /bin/ 3 | /test/tests.* 4 | /test/coverage.* 5 | !/dogfood/repository/ 6 | !/internal/repository/ 7 | /repository.yaml 8 | /blox 9 | /_build/ 10 | /dist/ 11 | cli/blox/blox 12 | /.github/.vscode/ 13 | dogfood/_build/ 14 | dogfood/data/_build/ 15 | vendor 16 | *.rpm 17 | *.deb 18 | !dummy.deb 19 | *.apk 20 | coverage.txt 21 | dist 22 | .DS_Store 23 | bin 24 | coverage.out 25 | /blox 26 | testdata/acceptance/tmp/ 27 | completions/ 28 | .envrc 29 | images_impl -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - thelper 4 | - gofumpt -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: blox 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | - ./scripts/completions.sh 7 | 8 | builds: 9 | - id: blox 10 | main: ./cmd/blox 11 | binary: blox 12 | # Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser`. 13 | ldflags: 14 | - -s -w -X main.version={{ .Version }} -X main.commit={{ .Commit }} -X main.date={{ .CommitDate }} -X main.builtBy=goreleaser 15 | goos: 16 | - linux 17 | - darwin 18 | - windows 19 | goarch: 20 | - amd64 21 | - arm64 22 | env: 23 | - GO111MODULE=on 24 | - MACOSX_DEPLOYMENT_TARGET=10.11 25 | 26 | dockers: 27 | - image_templates: 28 | - "ghcr.io/cueblox/blox:{{ .Tag }}-amd64" 29 | ids: 30 | - blox 31 | use_buildx: true 32 | build_flag_templates: 33 | - --platform=linux/amd64 34 | - image_templates: 35 | - "ghcr.io/cueblox/blox:{{ .Tag }}-arm64" 36 | ids: 37 | - blox 38 | use_buildx: true 39 | build_flag_templates: 40 | - --platform=linux/arm64 41 | 42 | docker_manifests: 43 | - name_template: "ghcr.io/cueblox/blox:latest" 44 | image_templates: 45 | - "ghcr.io/cueblox/blox:{{ .Tag }}-amd64" 46 | - "ghcr.io/cueblox/blox:{{ .Tag }}-arm64" 47 | 48 | - name_template: "ghcr.io/cueblox/blox:{{ .Tag }}" 49 | image_templates: 50 | - "ghcr.io/cueblox/blox:{{ .Tag }}-amd64" 51 | - "ghcr.io/cueblox/blox:{{ .Tag }}-arm64" 52 | 53 | changelog: 54 | filters: 55 | exclude: 56 | - Merge 57 | archives: 58 | - replacements: 59 | darwin: Darwin 60 | linux: Linux 61 | windows: Windows 62 | 386: i386 63 | amd64: x86_64 64 | format_overrides: 65 | - goos: windows 66 | format: zip 67 | files: 68 | - README.md 69 | - LICENSE 70 | - completions/* 71 | 72 | brews: 73 | - name: blox 74 | tap: 75 | owner: cueblox 76 | name: homebrew-tap 77 | token: "{{ .Env.TAP_WRITE_KEY }}" 78 | folder: Formula 79 | 80 | commit_author: 81 | name: CueBlox 82 | email: support@cueblox.com 83 | 84 | homepage: "https://github.com/cueblox/" 85 | description: "CueBlox" 86 | license: "MIT" 87 | test: | 88 | system "#{bin}/blox -v" 89 | install: |- 90 | bin.install "blox" 91 | bash_completion.install "completions/blox.bash" => "blox" 92 | zsh_completion.install "completions/blox.zsh" => "_blox" 93 | fish_completion.install "completions/blox.fish" 94 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "conventionalCommits.scopes": [ 3 | "dependencies" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at root@carlosbecker.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | By participating to this project, you agree to abide our [code of 4 | conduct](/CODE_OF_CONDUCT.md). 5 | 6 | ## Setup your machine 7 | 8 | `blox` is written in [Go](https://golang.org/). 9 | 10 | Prerequisites: 11 | 12 | * `make` 13 | * [Go 1.16+](https://golang.org/doc/install) 14 | 15 | Clone `blox` from source: 16 | 17 | ```sh 18 | $ git clone git@github.com:cueblox/blox.git 19 | $ cd blox 20 | ``` 21 | 22 | Install the build and lint dependencies: 23 | 24 | ```console 25 | $ make setup 26 | ``` 27 | 28 | A good way of making sure everything is all right is running the test suite: 29 | 30 | ```console 31 | $ make test 32 | ``` 33 | 34 | ## Test your change 35 | 36 | You can create a branch for your changes and try to build from the source as you go: 37 | 38 | ```console 39 | $ make build 40 | ``` 41 | 42 | When you are satisfied with the changes, we suggest you run: 43 | 44 | ```console 45 | $ make ci 46 | ``` 47 | 48 | Which runs all the linters and tests. 49 | 50 | ## Create a commit 51 | 52 | Commit messages should be well formatted. 53 | Start your commit message with the type. Choose one of the following: 54 | `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `revert`, `add`, `remove`, `move`, `bump`, `update`, `release` 55 | 56 | After a colon, you should give the message a title, starting with uppercase and ending without a dot. 57 | Keep the width of the text at 72 chars. 58 | The title must be followed with a newline, then a more detailed description. 59 | 60 | Please reference any GitHub issues on the last line of the commit message (e.g. `See #123`, `Closes #123`, `Fixes #123`). 61 | 62 | An example: 63 | 64 | ``` 65 | docs: Add example for --release-notes flag 66 | 67 | I added an example to the docs of the `--release-notes` flag to make 68 | the usage more clear. The example is an realistic use case and might 69 | help others to generate their own changelog. 70 | 71 | See #284 72 | ``` 73 | 74 | ## Submit a pull request 75 | 76 | Push your branch to your `blox` fork and open a pull request against the 77 | master branch. 78 | 79 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Required Tools 4 | 5 | * Go > 1.16 6 | * gofumpt `GO111MODULE=on go get mvdan.cc/gofumpt` 7 | * [golangci-lint](https://golangci-lint.run/usage/install/#local-installation) 8 | 9 | 10 | ## Building 11 | 12 | ```shell 13 | make setup # download go dependencies 14 | make build # or just make 15 | ``` 16 | 17 | ## Testing 18 | 19 | ```shell 20 | make test 21 | ``` 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | COPY blox / 4 | 5 | ENTRYPOINT ["/blox"] 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2021 Brian Ketelsen and David McKay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCE_FILES?=./... 2 | TEST_PATTERN?=. 3 | TEST_OPTIONS?= 4 | TEST_TIMEOUT?=15m 5 | TEST_PARALLEL?=2 6 | DOCKER_BUILDKIT?=1 7 | export DOCKER_BUILDKIT 8 | 9 | export PATH := ./bin:$(PATH) 10 | export GO111MODULE := on 11 | 12 | # Install all the build and lint dependencies 13 | setup: 14 | go mod tidy 15 | git config core.hooksPath .githooks 16 | go install mvdan.cc/gofumpt@latest 17 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /go/bin v1.44.2 18 | 19 | .PHONY: setup 20 | 21 | test: 22 | go test $(TEST_OPTIONS) -p $(TEST_PARALLEL) -v -failfast -race -coverpkg=./... -covermode=atomic -coverprofile=coverage.txt $(SOURCE_FILES) -run $(TEST_PATTERN) -timeout=$(TEST_TIMEOUT) 23 | .PHONY: test 24 | 25 | cover: test 26 | go tool cover -html=coverage.txt 27 | .PHONY: cover 28 | 29 | fmt: 30 | gofumpt -w . 31 | .PHONY: fmt 32 | 33 | lint: check 34 | golangci-lint run 35 | .PHONY: check 36 | 37 | ci: lint test 38 | .PHONY: ci 39 | 40 | build: 41 | go build -o blox ./cmd/blox/main.go 42 | ./scripts/cmd_docs.sh 43 | .PHONY: build 44 | 45 | install: 46 | go install ./cmd/blox 47 | 48 | deps: 49 | go get -u 50 | go mod tidy 51 | go mod verify 52 | .PHONY: deps 53 | 54 | serve: 55 | @docker run --rm -it -p 8000:8000 -v ${PWD}/dogfood/sites/docs:/docs squidfunk/mkdocs-material 56 | .PHONY: serve 57 | 58 | todo: 59 | @grep \ 60 | --exclude-dir=vendor \ 61 | --exclude-dir=node_modules \ 62 | --exclude-dir=bin \ 63 | --exclude=Makefile \ 64 | --text \ 65 | --color \ 66 | -nRo -E ' TODO:.*|SkipNow' . 67 | .PHONY: todo 68 | 69 | .DEFAULT_GOAL := build 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blox 2 | 3 | Blox is the CLI for working with [CueBlox](https://cueblox.com). 4 | 5 | ## What is Blox? 6 | 7 | See our rapidly-evolving documentation [here](https://github.com/cueblox/blox/blob/main/dogfood/data/pages/index.md) 8 | 9 | ## Vocabulary 10 | 11 | ### Blox 12 | 13 | A Blox is a collection of DataSets, grouped into a Schema, and distributed as a repository. 14 | 15 | These Blox can be consumed using the `blox` CLI to provide data validation and generation for your content repositories, ensuring type safety across your content. 16 | 17 | ### DataSet 18 | 19 | A DataSet is a type with a strongly defined schema, using [Cue](https://cuelang.org). 20 | 21 | See [examples](./dogfood/schemata/profile_v1.cue) 22 | 23 | ### Schema 24 | 25 | A Schema is a Cue file definition of one or more DataSets, with some metadata to help connect some dots for the `blox` CLI. 26 | 27 | See [examples](./dogfood/schemata) 28 | 29 | ### Repository 30 | 31 | Collection of schemas, distributed via HTTP with a `manifest.json`. Can be downloaded by the `blox` CLI. 32 | -------------------------------------------------------------------------------- /blox.cue: -------------------------------------------------------------------------------- 1 | { 2 | data_dir: "dogfood/data" 3 | schemata_dir: "dogfood/schemata" 4 | static_dir: "dogfood/static" 5 | } 6 | -------------------------------------------------------------------------------- /cmd/blox/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime/debug" 7 | 8 | "github.com/cueblox/blox/internal/cmd" 9 | ) 10 | 11 | // nolint: gochecknoglobals 12 | var ( 13 | version = "dev" 14 | commit = "" 15 | date = "" 16 | builtBy = "" 17 | ) 18 | 19 | func main() { 20 | cmd.Execute( 21 | buildVersion(version, commit, date, builtBy), 22 | os.Exit, 23 | os.Args[1:], 24 | ) 25 | } 26 | 27 | func buildVersion(version, commit, date, builtBy string) string { 28 | result := "blox version " + version 29 | if commit != "" { 30 | result = fmt.Sprintf("%s\ncommit: %s", result, commit) 31 | } 32 | if date != "" { 33 | result = fmt.Sprintf("%s\nbuilt at: %s", result, date) 34 | } 35 | if builtBy != "" { 36 | result = fmt.Sprintf("%s\nbuilt by: %s", result, builtBy) 37 | } 38 | if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" { 39 | result = fmt.Sprintf("%s\nmodule version: %s, checksum: %s", result, info.Main.Version, info.Main.Sum) 40 | } 41 | return result 42 | } 43 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package blox 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | 8 | "cuelang.org/go/cue" 9 | "github.com/pterm/pterm" 10 | ) 11 | 12 | type Config struct { 13 | runtime *Runtime 14 | } 15 | 16 | // New setup a new config type with 17 | // base as the defaults 18 | func NewConfig(base string) (*Config, error) { 19 | r, err := NewRuntimeWithBase(base) 20 | if err != nil { 21 | return nil, err 22 | } 23 | config := &Config{ 24 | runtime: r, 25 | } 26 | 27 | return config, nil 28 | } 29 | 30 | // LoadConfig opens the configuration file 31 | // specified in `path` and validates it against 32 | // the configuration provided in when the `Engine` 33 | // was initialized with `New()` 34 | func (r *Config) LoadConfig(path string) error { 35 | pterm.Debug.Printf("\t\tLoading config: %s\n", path) 36 | cueConfig, err := ioutil.ReadFile(path) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return r.LoadConfigString(string(cueConfig)) 42 | } 43 | 44 | func (r *Config) LoadConfigString(cueConfig string) error { 45 | cueValue := r.runtime.CueContext.CompileString(cueConfig) 46 | 47 | if cueValue.Err() != nil { 48 | return cueValue.Err() 49 | } 50 | 51 | r.runtime.Database = r.runtime.Database.Unify(cueValue) 52 | if err := r.runtime.Database.Validate(); err != nil { 53 | return err 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func (r *Config) GetString(key string) (string, error) { 60 | keyValue := r.runtime.Database.LookupPath(cue.ParsePath(key)) 61 | 62 | if keyValue.Exists() { 63 | return keyValue.String() 64 | } 65 | 66 | return "", fmt.Errorf("couldn't find key '%s'", key) 67 | } 68 | 69 | func (r *Config) GetBool(key string) (bool, error) { 70 | keyValue := r.runtime.Database.LookupPath(cue.ParsePath(key)) 71 | 72 | if keyValue.Exists() { 73 | return keyValue.Bool() 74 | } 75 | 76 | return false, fmt.Errorf("couldn't find key '%s'", key) 77 | } 78 | 79 | func (r *Config) GetList(key string) (cue.Value, error) { 80 | keyValue := r.runtime.Database.LookupPath(cue.ParsePath(key)) 81 | 82 | if keyValue.Exists() { 83 | _, err := keyValue.List() 84 | if err != nil { 85 | return cue.Value{}, err 86 | } 87 | 88 | return keyValue, nil 89 | } 90 | return cue.Value{}, errors.New("not found") 91 | } 92 | 93 | func (r *Config) GetStringOr(key string, def string) string { 94 | cueValue, err := r.GetString(key) 95 | if err != nil { 96 | return def 97 | } 98 | 99 | return cueValue 100 | } 101 | 102 | const BaseConfig = `{ 103 | #Remote: { 104 | name: string 105 | version: string 106 | repository: string 107 | } 108 | #Plugin: { 109 | name: string 110 | executable: string 111 | } 112 | build_dir: string | *"_build" 113 | data_dir: string | *"data" 114 | schemata_dir: string | *"schemata" 115 | static_dir: string | *"static" 116 | template_dir: string | *"templates" 117 | output_cue: bool | *false 118 | output_recordsets: bool | *false 119 | remotes: [ ...#Remote ] 120 | prebuild: [...#Plugin] 121 | postbuild: [...#Plugin] 122 | }` 123 | 124 | const DefaultConfigName = "blox.cue" 125 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package blox 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | const base = `{ 10 | build_dir: string | *"_build" 11 | data_dir: string | *"data" 12 | schemata_dir: string | *"schemata" 13 | static_dir: string | *"static" 14 | }` 15 | 16 | func TestGetString(t *testing.T) { 17 | runtime, err := NewConfig(base) 18 | if err != nil { 19 | t.Fatal("Failed to create a NewRuntime, which should never happen") 20 | } 21 | 22 | err = runtime.LoadConfigString(`{ 23 | data_dir: "my-data-dir" 24 | }`) 25 | assert.Equal(t, nil, err) 26 | 27 | // test non-existent key 28 | _, err = runtime.GetString("data_dir_non_exist") 29 | assert.NotEqual(t, nil, err) 30 | 31 | // test defaulted key 32 | schDir, err := runtime.GetString("schemata_dir") 33 | assert.Equal(t, nil, err) 34 | assert.Equal(t, "schemata", schDir) 35 | 36 | // test parsed key 37 | configString, err := runtime.GetString("data_dir") 38 | assert.Equal(t, nil, err) 39 | assert.Equal(t, "my-data-dir", configString) 40 | } 41 | -------------------------------------------------------------------------------- /content/graphql.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "sort" 8 | 9 | "github.com/cueblox/blox/internal/cuedb" 10 | "github.com/graphql-go/graphql" 11 | "github.com/graphql-go/handler" 12 | "github.com/pterm/pterm" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func (s *Service) prepGraphQL() error { 17 | if !s.built { 18 | err := s.build() 19 | if err != nil { 20 | return err 21 | } 22 | } 23 | 24 | dag := s.engine.GetDataSetsDAG() 25 | nodes, _ := dag.GetDescendants("root") 26 | 27 | // GraphQL API 28 | graphqlObjects := map[string]cuedb.GraphQlObjectGlue{} 29 | graphqlFields := graphql.Fields{} 30 | keys := make([]string, 0, len(nodes)) 31 | for k := range nodes { 32 | keys = append(keys, k) 33 | } 34 | sort.Strings(keys) 35 | vertexComplete := map[string]bool{} 36 | 37 | // iterate through all the root nodes 38 | for _, k := range keys { 39 | node := nodes[k] 40 | 41 | chNode, _, err := dag.DescendantsWalker(k) 42 | cobra.CheckErr(err) 43 | 44 | // first iterate through all the children 45 | // so dependencies are registered 46 | for nd := range chNode { 47 | n, err := dag.GetVertex(nd) 48 | cobra.CheckErr(err) 49 | if dg, ok := n.(*cuedb.DagNode); ok { 50 | _, ok := vertexComplete[dg.Name] 51 | if !ok { 52 | err := s.translateNode(n, graphqlObjects, graphqlFields) 53 | if err != nil { 54 | return err 55 | } 56 | vertexComplete[dg.Name] = true 57 | } 58 | } 59 | } 60 | // now process the parent node 61 | _, ok := vertexComplete[node.(*cuedb.DagNode).Name] 62 | if !ok { 63 | err := s.translateNode(node, graphqlObjects, graphqlFields) 64 | if err != nil { 65 | return err 66 | } 67 | vertexComplete[node.(*cuedb.DagNode).Name] = true 68 | } 69 | 70 | } 71 | 72 | queryType := graphql.NewObject( 73 | graphql.ObjectConfig{ 74 | Name: "Query", 75 | Fields: graphqlFields, 76 | }) 77 | 78 | schema, err := graphql.NewSchema( 79 | graphql.SchemaConfig{ 80 | Query: queryType, 81 | }, 82 | ) 83 | if err != nil { 84 | return err 85 | } 86 | s.schema = &schema 87 | return nil 88 | } 89 | 90 | func (s *Service) translateNode(node interface{}, graphqlObjects map[string]cuedb.GraphQlObjectGlue, graphqlFields map[string]*graphql.Field) error { 91 | dataSet, _ := s.engine.GetDataSet(node.(*cuedb.DagNode).Name) 92 | 93 | //nolint - ignore linter, we need this defined for the thunk to work 94 | var objType *graphql.Object 95 | 96 | objType = graphql.NewObject( 97 | graphql.ObjectConfig{ 98 | Name: dataSet.GetExternalName(), 99 | Fields: (graphql.FieldsThunk)(func() graphql.Fields { 100 | objectFields, err := cuedb.CueValueToGraphQlField(graphqlObjects, dataSet, dataSet.GetSchemaCue()) 101 | if err != nil { 102 | cobra.CheckErr(err) 103 | } 104 | 105 | // Inject ID field into each object 106 | objectFields["id"] = &graphql.Field{ 107 | Type: &graphql.NonNull{ 108 | OfType: graphql.String, 109 | }, 110 | } 111 | 112 | return objectFields 113 | }), 114 | }, 115 | ) 116 | 117 | resolver := func(p graphql.ResolveParams) (interface{}, error) { 118 | dataSetName := p.Info.ReturnType.Name() 119 | 120 | id, ok := p.Args["id"].(string) 121 | if ok { 122 | data := s.engine.GetAllData(fmt.Sprintf("#%s", dataSetName)) 123 | 124 | records := make(map[string]interface{}) 125 | if err := data.Decode(&records); err != nil { 126 | return nil, err 127 | } 128 | 129 | for recordID, record := range records { 130 | if string(recordID) == id { 131 | return record, nil 132 | } 133 | } 134 | } 135 | return nil, nil 136 | } 137 | 138 | graphqlObjects[dataSet.GetExternalName()] = cuedb.GraphQlObjectGlue{ 139 | Object: objType, 140 | Resolver: resolver, 141 | Engine: s.engine, 142 | } 143 | 144 | graphqlFields[dataSet.GetExternalName()] = &graphql.Field{ 145 | Name: dataSet.GetExternalName(), 146 | Type: objType, 147 | Args: graphql.FieldConfigArgument{ 148 | "id": &graphql.ArgumentConfig{ 149 | Type: graphql.String, 150 | }, 151 | }, 152 | Resolve: resolver, 153 | } 154 | 155 | graphqlFields[fmt.Sprintf("all%v", dataSet.GetPluralName())] = &graphql.Field{ 156 | Type: graphql.NewList(objType), 157 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 158 | dataSetName := p.Info.ReturnType.Name() 159 | 160 | data := s.engine.GetAllData(fmt.Sprintf("#%s", dataSetName)) 161 | 162 | records := make(map[string]interface{}) 163 | if err := data.Decode(&records); err != nil { 164 | return nil, err 165 | } 166 | 167 | values := []interface{}{} 168 | for _, value := range records { 169 | values = append(values, value) 170 | } 171 | 172 | return values, nil 173 | }, 174 | } 175 | return nil 176 | } 177 | 178 | // GQLHandlerFunc returns a stand alone graphql handler for use 179 | // in netlify/aws/azure serverless scenarios 180 | func (s *Service) GQLHandlerFunc() (http.HandlerFunc, error) { 181 | if s.schema == nil { 182 | err := s.prepGraphQL() 183 | if err != nil { 184 | return nil, err 185 | } 186 | } 187 | 188 | hf := func(w http.ResponseWriter, r *http.Request) { 189 | result := s.executeQuery(r.URL.Query().Get("query")) 190 | err := json.NewEncoder(w).Encode(result) 191 | if err != nil { 192 | pterm.Warning.Printf("failed to encode: %v", err) 193 | } 194 | } 195 | return hf, nil 196 | } 197 | 198 | // GQLPlaygroundHandler returns a stand alone graphql playground handler for use 199 | // in netlify/aws/azure serverless scenarios 200 | func (s *Service) GQLPlaygroundHandler() (http.Handler, error) { 201 | if s.schema == nil { 202 | err := s.prepGraphQL() 203 | if err != nil { 204 | return nil, err 205 | } 206 | } 207 | 208 | h := handler.New(&handler.Config{ 209 | Schema: s.schema, 210 | Pretty: true, 211 | GraphiQL: false, 212 | Playground: true, 213 | }) 214 | return h, nil 215 | } 216 | 217 | func (s *Service) executeQuery(query string) *graphql.Result { 218 | result := graphql.Do(graphql.Params{ 219 | Schema: *s.schema, 220 | RequestString: query, 221 | }) 222 | 223 | if len(result.Errors) > 0 { 224 | pterm.Error.Printf("errors: %v\n", result.Errors) 225 | } 226 | 227 | return result 228 | } 229 | -------------------------------------------------------------------------------- /content/postplugins.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/cueblox/blox/plugins" 9 | "github.com/cueblox/blox/plugins/shared" 10 | "github.com/hashicorp/go-hclog" 11 | "github.com/hashicorp/go-plugin" 12 | "github.com/pterm/pterm" 13 | ) 14 | 15 | func (s *Service) runPostPlugins() error { 16 | post, err := s.Cfg.GetList("postbuild") 17 | if err != nil { 18 | return err 19 | } 20 | iter, err := post.List() 21 | if err != nil { 22 | return err 23 | } 24 | for iter.Next() { 25 | val := iter.Value() 26 | 27 | //nolint 28 | name, err := val.FieldByName("name", false) 29 | if err != nil { 30 | return err 31 | } 32 | n, err := name.Value.String() 33 | if err != nil { 34 | return err 35 | } 36 | pterm.Info.Println("Registering postbuild plugin", n) 37 | 38 | //nolint 39 | exec, err := val.FieldByName("executable", false) 40 | if err != nil { 41 | return err 42 | } 43 | e, err := exec.Value.String() 44 | if err != nil { 45 | return err 46 | } 47 | postPluginMap[n] = &plugins.PostbuildPlugin{} 48 | pterm.Info.Println("Calling Plugin", n, e) 49 | err = s.callPostPlugin(n, e) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | } 55 | return nil 56 | } 57 | 58 | func (s *Service) callPostPlugin(name, executable string) error { 59 | pterm.Info.Println("calling the plugin") 60 | // Create an hclog.Logger 61 | logger := hclog.New(&hclog.LoggerOptions{ 62 | Name: "plugin", 63 | Output: os.Stdout, 64 | Level: hclog.Info, 65 | }) 66 | executablePath, err := exec.LookPath(executable) 67 | if err != nil { 68 | pterm.Error.Println("plugin not found in path", executable) 69 | log.Fatal(err) 70 | } 71 | pterm.Info.Println("found plugin at path", executable, executablePath) 72 | // We're a host! Start by launching the plugin process. 73 | client := plugin.NewClient(&plugin.ClientConfig{ 74 | HandshakeConfig: shared.PostbuildHandshakeConfig, 75 | Plugins: postPluginMap, 76 | Cmd: exec.Command(executablePath), 77 | Logger: logger, 78 | }) 79 | defer client.Kill() 80 | 81 | // Connect via RPC 82 | log.Println("Connecting to client") 83 | rpcClient, err := client.Client() 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | 88 | // Request the plugin 89 | log.Println("Getting Plugin") 90 | raw, err := rpcClient.Dispense(name) 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | 95 | // We should have a Postbuild plugin now! This feels like a normal interface 96 | // implementation but is in fact over an RPC connection. 97 | postplug := raw.(plugins.Postbuild) 98 | log.Println("Making the call") 99 | log.Println(s.rawConfig) 100 | return postplug.Process(s.rawConfig) 101 | } 102 | -------------------------------------------------------------------------------- /content/preplugins.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | 8 | "github.com/cueblox/blox/plugins" 9 | "github.com/cueblox/blox/plugins/shared" 10 | "github.com/hashicorp/go-hclog" 11 | "github.com/hashicorp/go-plugin" 12 | "github.com/pterm/pterm" 13 | ) 14 | 15 | func (s *Service) runPrePlugins() error { 16 | pre, err := s.Cfg.GetList("prebuild") 17 | if err != nil { 18 | return err 19 | } 20 | iter, err := pre.List() 21 | if err != nil { 22 | return err 23 | } 24 | for iter.Next() { 25 | val := iter.Value() 26 | 27 | //nolint 28 | name, err := val.FieldByName("name", false) 29 | if err != nil { 30 | return err 31 | } 32 | n, err := name.Value.String() 33 | if err != nil { 34 | return err 35 | } 36 | pterm.Info.Println("Registering prebuild plugin", n) 37 | //nolint 38 | exec, err := val.FieldByName("executable", false) 39 | if err != nil { 40 | return err 41 | } 42 | e, err := exec.Value.String() 43 | if err != nil { 44 | return err 45 | } 46 | prePluginMap[n] = &plugins.PrebuildPlugin{} 47 | pterm.Info.Println("Calling Plugin", n, e) 48 | err = s.callPrePlugin(n, e) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | } 54 | return nil 55 | } 56 | 57 | func (s *Service) callPrePlugin(name, executable string) error { 58 | // Create an hclog.Logger 59 | logger := hclog.New(&hclog.LoggerOptions{ 60 | Name: "plugin", 61 | Output: os.Stdout, 62 | Level: hclog.Info, 63 | }) 64 | 65 | executablePath, err := exec.LookPath(executable) 66 | if err != nil { 67 | pterm.Error.Println("plugin not found in path", executable) 68 | log.Fatal(err) 69 | } 70 | pterm.Info.Println("found plugin at path", executable, executablePath) 71 | // We're a host! Start by launching the plugin process. 72 | client := plugin.NewClient(&plugin.ClientConfig{ 73 | HandshakeConfig: shared.PrebuildHandshakeConfig, 74 | Plugins: prePluginMap, 75 | Cmd: exec.Command(executablePath), 76 | Logger: logger, 77 | }) 78 | defer client.Kill() 79 | 80 | // Connect via RPC 81 | rpcClient, err := client.Client() 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | 86 | // Request the plugin 87 | raw, err := rpcClient.Dispense(name) 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | // We should have a Prebuild plugin now! This feels like a normal interface 93 | // implementation but is in fact over an RPC connection. 94 | preplug := raw.(plugins.Prebuild) 95 | return preplug.Process(s.rawConfig) 96 | } 97 | -------------------------------------------------------------------------------- /content/render.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | 9 | "github.com/pterm/pterm" 10 | ) 11 | 12 | func (s *Service) RenderJSON() ([]byte, error) { 13 | if !s.built { 14 | err := s.build() 15 | if err != nil { 16 | return nil, err 17 | } 18 | } 19 | pterm.Debug.Println("Building output data blox") 20 | output, err := s.engine.GetOutput() 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | pterm.Debug.Println("Rendering data blox to JSON") 26 | return output.MarshalJSON() 27 | } 28 | 29 | func (s *Service) RenderAndSave() error { 30 | if !s.built { 31 | err := s.build() 32 | if err != nil { 33 | return err 34 | } 35 | } 36 | 37 | bb, err := s.RenderJSON() 38 | if err != nil { 39 | return err 40 | } 41 | buildDir, err := s.Cfg.GetString("build_dir") 42 | if err != nil { 43 | return err 44 | } 45 | err = os.MkdirAll(buildDir, 0o755) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | build_cue, err := s.Cfg.GetBool("output_cue") 51 | if err != nil { 52 | return err 53 | } 54 | // only output the cue if it's specified in the config file 55 | if build_cue { 56 | cval, err := s.engine.MarshalString() 57 | if err != nil { 58 | pterm.Error.Println("error getting cue output") 59 | return err 60 | } 61 | cval = "package blox\n" + cval 62 | 63 | err = os.WriteFile(filepath.Join(buildDir, "data.cue"), []byte(cval), 0o755) 64 | if err != nil { 65 | return err 66 | } 67 | pterm.Success.Printf("Cue output written to '%s'\n", filepath.Join(buildDir, "data.cue")) 68 | 69 | } 70 | // always output the full json dataset 71 | filename := "data.json" 72 | filePath := path.Join(buildDir, filename) 73 | err = os.WriteFile(filePath, bb, 0o755) 74 | if err != nil { 75 | return err 76 | } 77 | pterm.Success.Printf("Data blox written to '%s'\n", filePath) 78 | 79 | build_recordsets, err := s.Cfg.GetBool("output_recordsets") 80 | if err != nil { 81 | return err 82 | } 83 | if build_recordsets { 84 | 85 | var dataList map[string][]map[string]interface{} 86 | 87 | err = json.Unmarshal(bb, &dataList) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | for k := range dataList { 93 | set := dataList[k] 94 | ss, err := json.Marshal(set) 95 | if err != nil { 96 | return err 97 | } 98 | filename := k + ".json" 99 | filePath := path.Join(buildDir, filename) 100 | 101 | // write the array 102 | err = os.WriteFile(filePath, ss, 0o755) 103 | if err != nil { 104 | return err 105 | } 106 | dirpath := path.Join(buildDir, k) 107 | err = os.MkdirAll(dirpath, 0o755) 108 | if err != nil { 109 | if err != os.ErrExist { 110 | return err 111 | } 112 | } 113 | for j := range set { 114 | slug := set[j]["id"].(string) 115 | // write each item 116 | filename := slug + ".json" 117 | filePath := path.Join(dirpath, filename) 118 | derp := path.Dir(filePath) 119 | err = os.MkdirAll(derp, 0o755) 120 | if err != nil { 121 | if err != os.ErrExist { 122 | return err 123 | } 124 | } 125 | ss, err := json.Marshal(set[j]) 126 | if err != nil { 127 | return err 128 | } 129 | err = os.WriteFile(filePath, ss, 0o755) 130 | if err != nil { 131 | return err 132 | } 133 | } 134 | 135 | } 136 | pterm.Success.Printf("Recordset output written to '%s'\n", buildDir) 137 | 138 | } 139 | 140 | pterm.Info.Println("Running Postbuild Plugins") 141 | err = s.runPostPlugins() 142 | if err != nil { 143 | return err 144 | } 145 | return nil 146 | } 147 | -------------------------------------------------------------------------------- /content/rest.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/pterm/pterm" 9 | ) 10 | 11 | // RestHandlerFunc returns a handler function that will render 12 | // the dataset specified as the last path parameter. 13 | func (s *Service) RestHandlerFunc() (http.HandlerFunc, error) { 14 | if !s.built { 15 | err := s.build() 16 | if err != nil { 17 | return nil, err 18 | } 19 | } 20 | 21 | hf := func(w http.ResponseWriter, r *http.Request) { 22 | w.Header().Set("Content-Type", "application/json") 23 | w.Header().Set("Access-Control-Allow-Origin", "*") 24 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type") 25 | w.Header().Set("Access-Control-Allow-Methods", "GET") 26 | 27 | path := strings.TrimPrefix(r.URL.Path, "/") 28 | parts := strings.Split(path, "/") 29 | pterm.Debug.Println(parts, len(parts)) 30 | if len(parts) == 0 { 31 | pterm.Warning.Println("No dataset specified") 32 | w.WriteHeader(http.StatusNotFound) 33 | return 34 | } 35 | dataset := parts[len(parts)-1] 36 | 37 | ds, err := s.engine.GetDataSetByPlural(dataset) 38 | if err != nil { 39 | pterm.Warning.Println("Requested dataset not found", parts, len(parts)) 40 | w.WriteHeader(http.StatusNotFound) 41 | return 42 | } 43 | 44 | data := s.engine.GetAllData(ds.GetExternalName()) 45 | err = json.NewEncoder(w).Encode(data) 46 | if err != nil { 47 | pterm.Warning.Printf("failed to encode: %v", err) 48 | } 49 | } 50 | return hf, nil 51 | } 52 | -------------------------------------------------------------------------------- /dogfood/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /dogfood/blox.cue: -------------------------------------------------------------------------------- 1 | { 2 | data_dir: "data" 3 | schemata_dir: "schemata" 4 | template_dir: "data/tpl" 5 | static_dir: "static" 6 | output_cue: true 7 | output_recordsets: false 8 | } 9 | -------------------------------------------------------------------------------- /dogfood/data/articles/another.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Another Article 3 | category: introduction 4 | profile: bketelsen 5 | excerpt: Autem aliquam labore aspernatur accusantium harum facere. 6 | featured: true 7 | publish_date: "2021-03-19" 8 | last_edit_date: "2021-03-21" 9 | edit_description: Hello HN, thanks for stopping by! 10 | tags: 11 | - one 12 | - two 13 | image: jpg/main-img-preview 14 | --- 15 | 16 | Placeat consequuntur ullam aut sapiente illo velit. Eius facere ut molestias totam laborum pariatur quam. Praesentium quo veritatis expedita animi. 17 | 18 | Quite anything glass benefit. Such form clearly top tend can require my. Federal degree sort performance region maintain. 19 | 20 | Ut dignissimos sapiente culpa rerum pariatur consequatur. Corporis suscipit ad corrupti aut. Expedita culpa aut deleniti officiis. 21 | 22 | Porro eum id sit quia expedita. Alias expedita asperiores. Corporis ex eum atque cum ea. 23 | 24 | More to come soon! 25 | -------------------------------------------------------------------------------- /dogfood/data/articles/hello-world.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hello World 3 | category: introduction 4 | profile: bketelsen 5 | excerpt: Autem aliquam labore aspernatur accusantium harum facere. 6 | featured: true 7 | publish_date: "2021-03-19" 8 | last_edit_date: "2021-03-21" 9 | edit_description: Hello HN, thanks for stopping by! 10 | tags: 11 | - one 12 | - two 13 | image: jpg/main-img-preview 14 | --- 15 | 16 | Placeat consequuntur ullam aut sapiente illo velit. Eius facere ut molestias totam laborum pariatur quam. Praesentium quo veritatis expedita animi. 17 | 18 | Quite anything glass benefit. Such form clearly top tend can require my. Federal degree sort performance region maintain. 19 | 20 | Ut dignissimos sapiente culpa rerum pariatur consequatur. Corporis suscipit ad corrupti aut. Expedita culpa aut deleniti officiis. 21 | 22 | Porro eum id sit quia expedita. Alias expedita asperiores. Corporis ex eum atque cum ea. 23 | 24 | More to come soon! 25 | -------------------------------------------------------------------------------- /dogfood/data/categories/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Introduction 3 | description: Introduction to CueBlox 4 | --- 5 | 6 | Information about the Introduction Category Here 7 | -------------------------------------------------------------------------------- /dogfood/data/images/jpg/main-img-preview.yaml: -------------------------------------------------------------------------------- 1 | file_name: images/jpg/main-img-preview.jpg 2 | height: 1188 3 | width: 2272 4 | cdn: "" 5 | -------------------------------------------------------------------------------- /dogfood/data/images/png/bketelsens-report.yaml: -------------------------------------------------------------------------------- 1 | file_name: images/png/bketelsens-report.png 2 | height: 700 3 | width: 600 4 | cdn: "" 5 | -------------------------------------------------------------------------------- /dogfood/data/pages/about.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About 3 | excerpt: Autem aliquam labore aspernatur accusantium harum facere. 4 | publish_date: "2021-03-19" 5 | tags: 6 | - one 7 | - two 8 | weight: 1 9 | --- 10 | 11 | Placeat consequuntur ullam aut sapiente illo velit. Eius facere ut molestias totam laborum pariatur quam. Praesentium quo veritatis expedita animi. 12 | 13 | Quite anything glass benefit. Such form clearly top tend can require my. Federal degree sort performance region maintain. 14 | 15 | Ut dignissimos sapiente culpa rerum pariatur consequatur. Corporis suscipit ad corrupti aut. Expedita culpa aut deleniti officiis. 16 | 17 | Porro eum id sit quia expedita. Alias expedita asperiores. Corporis ex eum atque cum ea. 18 | 19 | Change 20 | -------------------------------------------------------------------------------- /dogfood/data/pages/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | weight: 0 4 | excerpt: CueBlox documentation 5 | publish_date: "2021-03-19" 6 | --- 7 | 8 | # CueBlox 9 | 10 | CueBlox is a set of tools that allow you to create and consume datasets from YAML or Markdown files. 11 | 12 | At the core is a tool that aggregates similar files into collections of data. If you've ever built a website with a static site generator like Hugo, this will be familiar. 13 | 14 | Where CueBlox really shines though is in the additional functionality it enables. CueBlox has several features that enable some interesting and novel integrations for your data: 15 | 16 | - Validate your datasets against a schema. 17 | 18 | Ensure your data is always valid by providing defaults and validation rules using the [Cue](https://cuelang.org) language to define schemata for your data. 19 | 20 | The schema that validates this page looks like this: 21 | 22 | ``` 23 | #Page: { 24 | _dataset: { 25 | plural: "pages" 26 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 27 | } 28 | 29 | title: string 30 | excerpt: string 31 | draft: bool | *false 32 | publish_date: string 33 | image?: string 34 | body?: string 35 | tags?: [...string] 36 | section_id?: string 37 | weight?: int 38 | } 39 | ``` 40 | 41 | In this schema we've defined required fields and optional fields by adding or omitting the `?` in the field definition. A `Page` must have a `title`, but `weight` is optional. 42 | 43 | - Aggregate your datasets and export them into a JSON file 44 | 45 | Data that is locked in your git repository is only useful in that repository. CueBlox allows you to validate, aggregate, and export your data in the integration-friendly JSON format for consumption elsewhere. The data that drives this website is processed through CueBlox and automatically published as a GitHub release. You can see it [here](https://github.com/cueblox/blox/releases/tag/blox) 46 | 47 | - Leverage third party tools to make your data easily accessible 48 | 49 | CueBlox includes a `hosting` command that generates a website suitable for deployment on Azure Static Web Apps, Vercel, or Netlify. The site includes a serverless function powered by [json-graphql-server](https://github.com/marmelab/json-graphql-server). The serverless function pulls the data from a URL (the GitHub release mentioned above) and serves it over GraphQL as a read-only dataset. By following a [few conventions](https://github.com/marmelab/json-graphql-server#generated-types-and-queries), we even get real referential integrity between our markdown files. You can play with the GraphQL server that powers this site [here](https://api.cueblox.com/api/graphql). Click on the "Docs" link at the top and see all the wonderful GraphQL data that is generated from our directories of Markdown files. 50 | 51 | - Create and consume standardized schemata for you and your team 52 | 53 | The schemata you create are available to you locally in your content directory. But you can also create a set of schemata that is published for others to consume. The schema we use to build all of the CueBlox website are published on [this website](https://schemas.cueblox.com/) All the information about the schemata available, including version information is available in the `manifest.json` file linked in the HTML. 54 | 55 | The `blox` cli tool allows you to add these remote schemata to your project: 56 | 57 | ``` 58 | ❯ blox remote list schemas.cueblox.com 59 | Namespace | Schema | Version 60 | schemas.cueblox.com | article | v1 61 | schemas.cueblox.com | category | v1 62 | schemas.cueblox.com | page | v1 63 | schemas.cueblox.com | profile | v1 64 | schemas.cueblox.com | section | v1 65 | schemas.cueblox.com | website | v1 66 | 67 | ❯ blox remote get schemas.cueblox.com page v1 68 | ``` 69 | 70 | Using published schemata this way allows you to create and consume standardized data across several projects, teams, and people. In fact, it is the core driver for our development of CueBlox -- enabling teams to publish information individually, but have it aggregated and consumed at a higher level without data validation worries. When everyone uses the same schema, all the data is always consistent. 71 | 72 | - Leverage all of this functionality using GitOps principles 73 | 74 | CueBlox was built for lazy people. If it can be generated, we generate it. If it can be automated, we automate it. The end result is that the only thing you need to do to publish your content is check it into your git repository. GitHub actions take care of the rest! 75 | -------------------------------------------------------------------------------- /dogfood/data/pages/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | excerpt: What is CueBlox 4 | publish_date: "2021-03-19" 5 | section: introduction 6 | weight: 1 7 | --- 8 | 9 | # Introduction 10 | 11 | CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents. 12 | 13 | ## Motivation 14 | 15 | We built CueBlox out of a desire to share data in a group or team setting without worrying about incompatible metadata. Markdown (with YAML frontmatter) and just plain YAML are great tools for content authors, but we were missing a reliable way of ensuring the content created by different people -- possibly in different repositories -- has the same metadata. Agreeing to use a common template for your frontmatter is a good start, but even that falls short when inconsistencies in metadata values creep in. 16 | 17 | With data consistency at the core, our next concern was making the data easily accessible. We explored methods of serving content repositories over GraphQL and REST by trying to build a complicated server that syncs remote git repositories to server temp storage, parses and validates them, then serves the combined data over GraphQL. While technically possible, there were many moving parts and it felt like too much manual work for a task that should be relatively simple. 18 | 19 | Finally, we wanted to make it trivial to make a content repository available for consumption publicly or privately over industry-standard protocols. You shouldn't have to install custom tools to consume data. It should be available in standard encoding formats over standard transfer protocols. 20 | 21 | ## The CueBlox Solution 22 | 23 | CueBlox provides a scaffold system that allows you to create a content repository with well-defined schemata. Schema definitions are stored with the content so that any consumer of the content has the information required to validate and parse the content. 24 | 25 | Schema definitions are written in [Cue](https://cuelang.org), providing a powerful method of defining field-level requirements and providing default values for fields. 26 | 27 | CueBlox also supports a convention-based relational data system by automatically linking and validating foreign keys which follow basic naming conventions. A document can reference a document of another type by adding a foreign key field to the metadata. Much like the relational database concepts on which this is based, CueBlox will validate that the record referenced in your foreign key field exists. This allows documents of different schemas to reference relational data in a familiar format without having to specify relationships in configuration files. 28 | 29 | A concrete example of this is the typical blog post. A blog post written in Markdown might have YAML metadata in the frontmatter that describes the post and provides additional information which is used to format and display the post. Here's a the frontmatter for the page you're reading now: 30 | 31 | ```yaml 32 | title: Introduction 33 | excerpt: What is CueBlox 34 | publish_date: "2021-03-19" 35 | section_id: introduction 36 | weight: 1 37 | ``` 38 | 39 | This frontmatter lives in a file called `introduction.md` in the `pages` folder of our content repository. The `section_id` field is a foreign key reference to a document in the `sections` folder which is named `introduction.md`. In relational database terms, the Introduction page belongs to the Introduction section. `Sections` have many `Pages`, `Pages` belong to `Sections`. When validating the content, CueBlox will ensure that the `Section` referenced actually exists, and throw an error if it doesn't. 40 | 41 | Field-level validations and relational validations ensure that your data exists in the shape you intended, without common errors. Most publishing platforms will happily allow you to have frontmatter fields that are not known to the system consuming them. If you mis-spell `excerpt` in your frontmatter, you're not likely to know it until the publishing system gets the data and there's no summary in the listing page. CueBlox prevents this by giving you the ability to define required and optional fields, and allowing you to "close" a definition, preventing extra fields from being added. 42 | 43 | ## Beyond Validation 44 | 45 | Providing schema and relational validation is the foundation of CueBlox, but it only solves the consistency issue. The next layer of CueBlox takes a content repository and assembles it into a JSON object that can be consumed anywhere. You can assemble your content into JSON locally next the applications that consume it, or you can use other tools to serve that data over the Internet. 46 | 47 | CueBlox provides pre-built examples that allow you to build automated content deployment pipelines hosted on your favorite cloud. We've built easy to integrate GitHub actions to automate validation and deployment, as well. In a matter of minutes you can publish a content repository over GraphQL and REST APIs with automatic deployments on all the major hosting providers. CueBlox was built to be deployed inexpensively, and it's likely that your favorite hosting provider has a free tier of hosting that will be more than sufficient for CueBlox's minimal hosting requirements. 48 | 49 | ## Teamwork Makes the Dream Work 50 | 51 | A fundamental problem we set out to solve was collaboration across teams of any size. CueBlox enables this by allowing teams to create and publish their own Schemata, including built-in support for versioning. With shared schemata, your team can be confident that everyone will create data that can be consumed and aggregated without manual intervention. 52 | 53 | ## Beyond The Blog 54 | 55 | While the toolset is well suited for managing markdown content that you intend to publish, that's far from the only use case we envisioned. One of our primary goals was to allow a git-based workflow for other types of data. As a concrete example, the primary authors of CueBlox are in Developer Relations. We intend to create shared schemata for our teams to allow us to share information about events, speaking engagements, conferences and other types of data that might be aggregated to create a team calendar, or provide metrics for management. Because CueBlox is markdown or YAML stored in a git repository, we can even enable git workflows for approval. Imagine creating a travel request by submitting a PR to a shared team content repository. Approval and merging automatically create the data about the event and travel costs. Your workflows are only limited by your imagination, but CueBlox enables everyone to create and consume the data. 56 | -------------------------------------------------------------------------------- /dogfood/data/pages/managing-content.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Managing Content 3 | excerpt: Manage your content 4 | publish_date: "2021-03-19" 5 | section: manage-content 6 | weight: 0 7 | --- 8 | 9 | # blox command 10 | 11 | Installation and quick start instructions 12 | 13 | ## Why Use CueBlox 14 | 15 | ## What can it be used for? 16 | -------------------------------------------------------------------------------- /dogfood/data/profiles/bketelsen.md: -------------------------------------------------------------------------------- 1 | --- 2 | first_name: Brian 3 | last_name: Ketelsen 4 | company: Microsoft 5 | title: Principal Cloud Developer Advocate 6 | --- 7 | 8 | Livin' on the Edge 9 | -------------------------------------------------------------------------------- /dogfood/data/sections/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Getting Started 3 | description: Getting Started with CueBlox 4 | weight: 1 5 | --- 6 | 7 | How to get started 8 | -------------------------------------------------------------------------------- /dogfood/data/sections/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Introduction 3 | description: Introduction to CueBlox 4 | weight: 0 5 | --- 6 | -------------------------------------------------------------------------------- /dogfood/data/sections/manage-content.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Manage Content 3 | description: How to manage your content 4 | weight: 2 5 | --- 6 | 7 | # How to get started 8 | -------------------------------------------------------------------------------- /dogfood/data/tpl/article.txt.tmpl: -------------------------------------------------------------------------------- 1 | Title:{{ .title }} 2 | -------------------------------------------------------------------------------- /dogfood/data/tpl/articles.txt.tmpl: -------------------------------------------------------------------------------- 1 | {{ range . -}} 2 | Title:{{ .title }} 3 | {{- end }} 4 | -------------------------------------------------------------------------------- /dogfood/data/tpl/brian.txt.tmpl: -------------------------------------------------------------------------------- 1 | {{ with .articles -}} 2 | {{ range . -}} 3 | Title:{{ .title }} 4 | {{- end }} 5 | {{- end }} -------------------------------------------------------------------------------- /dogfood/data/websites/www.yaml: -------------------------------------------------------------------------------- 1 | url: https://www.cueblox.com 2 | profile: bketelsen 3 | -------------------------------------------------------------------------------- /dogfood/repository.cue: -------------------------------------------------------------------------------- 1 | {"repository_root":"repository","namespace":"schemas.cueblox.com","output_dir":"_build"} -------------------------------------------------------------------------------- /dogfood/repository/_build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | No content here 5 | Manifest 6 | 7 | 8 | -------------------------------------------------------------------------------- /dogfood/repository/_build/manifest.json: -------------------------------------------------------------------------------- 1 | {"Root":"repository","Namespace":"schemas.cueblox.com","Output":"_build","Schemas":[{"Namespace":"schemas.cueblox.com","Name":"article","Versions":[{"Namespace":"schemas.cueblox.com","Name":"v1","Schema":"article","Definition":"{\n\t_schema: {\n\t\tname: \"Article\"\n\t\tnamespace: \"schemas.cueblox.com\"\n\t}\n\n\t#Article: {\n\t\t_dataset: {\n\t\t\tplural: \"articles\"\n\t\t\tsupportedExtensions: [\"yaml\", \"yml\", \"md\", \"mdx\"]\n\t\t}\n\n\t\ttitle: string @template(\"My New Article\")\n\t\texcerpt: string @template(\"Small Description\")\n\t\tfeatured: bool | *false\n\t\tdraft: bool | *false\n\t\tpublish_date: string @template(\"2020-01-01\")\n\t\timage?: string\n\t\tlast_edit_date?: string\n\t\tedit_description?: string\n\t\tbody?: string @template(\"My Awesome Article\")\n\t\ttags?: [...string]\n\t\tcategory_id?: string\n\t\tprofile_id?: string\n\t}\n\n}\n"}]},{"Namespace":"schemas.cueblox.com","Name":"category","Versions":[{"Namespace":"schemas.cueblox.com","Name":"v1","Schema":"category","Definition":"{\n\t_schema: {\n\t\tname: \"Category\"\n\t\tnamespace: \"schemas.cueblox.com\"\n\t}\n\n\t#Category: {\n\t\t_dataset: {\n\t\t\tplural: \"categories\"\n\t\t\tsupportedExtensions: [\"yaml\", \"yml\", \"md\", \"mdx\"]\n\t\t}\n\n\t\tname: string @template(\"Name\")\n\t\tdescription: string @template(\"Description\")\n\t\tbody?: string @template(\"This is my category for ...\")\n\t}\n\n}\n"}]},{"Namespace":"schemas.cueblox.com","Name":"page","Versions":[{"Namespace":"schemas.cueblox.com","Name":"v1","Schema":"page","Definition":"{\n\t_schema: {\n\t\tname: \"Page\"\n\t\tnamespace: \"schemas.cueblox.com\"\n\t}\n\n\t#Page: {\n\t\t_dataset: {\n\t\t\tplural: \"pages\"\n\t\t\tsupportedExtensions: [\"yaml\", \"yml\", \"md\", \"mdx\"]\n\t\t}\n\n\t\ttitle: string @template(\"My New Page\")\n\t\texcerpt: string @template(\"Small description about my page\")\n\t\tdraft: bool | *false\n\t\tpublish_date: string @template(\"2020-01-01\")\n\t\timage?: string\n\t\tbody?: string\n\t\ttags?: [...string]\n\t\tsection_id?: string\n\t\tweight?: int\n\t}\n\n}\n"}]},{"Namespace":"schemas.cueblox.com","Name":"profile","Versions":[{"Namespace":"schemas.cueblox.com","Name":"v1","Schema":"profile","Definition":"{\n\t_schema: {\n\t\tname: \"Profile\"\n\t\tnamespace: \"schemas.cueblox.com\"\n\t}\n\n\t#Profile: {\n\t\t_dataset: {\n\t\t\tplural: \"profiles\"\n\t\t\tsupportedExtensions: [\"yaml\", \"yml\", \"md\", \"mdx\"]\n\t\t}\n\n\t\tfirst_name: string @template(\"Forename\")\n\t\tlast_name: string @template(\"Surname\")\n\t\tage?: int @template(21)\n\t\tcompany?: string @template(\"CueBlox\")\n\t\ttitle?: string @template(\"Cue Slinger\")\n\t\tbody?: string @template(\"☕️ Required\")\n\t\tsocial_accounts?: [...#TwitterAccount | #GitHubAccount | #MiscellaneousAccount]\n\t}\n\n\t#TwitterAccount: {\n\t\tnetwork: \"twitter\"\n\t\tusername: string @template(\"twitter-handle\")\n\t\turl: *\"https://twitter.com/\\(username)\" | string\n\t}\n\n\t#GitHubAccount: {\n\t\tnetwork: \"github\"\n\t\tusername: string @template(\"github-handle\")\n\t\turl: *\"https://github.com/\\(username)\" | string\n\t}\n\n\t#MiscellaneousAccount: {\n\t\tnetwork: string @template(\"some_network\")\n\t\turl: string @template(\"https://some_url\")\n\t}\n}\n"}]},{"Namespace":"schemas.cueblox.com","Name":"section","Versions":[{"Namespace":"schemas.cueblox.com","Name":"v1","Schema":"section","Definition":"{\n\t_schema: {\n\t\tname: \"Section\"\n\t\tnamespace: \"schemas.cueblox.com\"\n\t}\n\n\t#Section: {\n\t\t_dataset: {\n\t\t\tplural: \"sections\"\n\t\t\tsupportedExtensions: [\"yaml\", \"yml\", \"md\", \"mdx\"]\n\t\t}\n\n\t\tname: string @template(\"Name\")\n\t\tdescription: string @template(\"Small description\")\n\t\tbody?: string @template(\"All about this section\")\n\t\tweight?: int | *0\n\t}\n\n}\n"}]},{"Namespace":"schemas.cueblox.com","Name":"website","Versions":[{"Namespace":"schemas.cueblox.com","Name":"v1","Schema":"website","Definition":"{\n\t_schema: {\n\t\tname: \"Profile\"\n\t\tnamespace: \"schemas.cueblox.com\"\n\t}\n\n\t#Website: {\n\t\t_dataset: {\n\t\t\tplural: \"websites\"\n\t\t\tsupportedExtensions: [\"yaml\", \"yml\"]\n\t\t}\n\n\t\turl: string @template(\"https://google.com\")\n\t\tprofile_id?: string\n\t\tbody?: string\n\t}\n}\n"}]}]} -------------------------------------------------------------------------------- /dogfood/repository/article/v1/schema.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Article" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Article: { 8 | _dataset: { 9 | plural: "articles" 10 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 11 | } 12 | 13 | title: string @template("My New Article") 14 | excerpt: string @template("Small Description") 15 | featured: bool | *false 16 | draft: bool | *false 17 | publish_date: string @template("2020-01-01") 18 | image_id?: string 19 | last_edit_date?: string 20 | edit_description?: string 21 | body?: string @template("My Awesome Article") 22 | tags?: [...string] 23 | category_id?: string 24 | profile_id?: string 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /dogfood/repository/category/v1/schema.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Category" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Category: { 8 | _dataset: { 9 | plural: "categories" 10 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 11 | } 12 | 13 | name: string @template("Name") 14 | description: string @template("Description") 15 | image_id?: string 16 | body?: string @template("This is my category for ...") 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /dogfood/repository/page/v1/schema.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Page" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Page: { 8 | _dataset: { 9 | plural: "pages" 10 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 11 | } 12 | 13 | title: string @template("My New Page") 14 | excerpt: string @template("Small description about my page") 15 | draft: bool | *false 16 | publish_date: string @template("2020-01-01") 17 | image_id?: string 18 | body?: string 19 | tags?: [...string] 20 | section_id?: string 21 | weight?: int 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /dogfood/repository/profile/v1/schema.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Profile" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Profile: { 8 | _dataset: { 9 | plural: "profiles" 10 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 11 | } 12 | 13 | first_name: string @template("Forename") 14 | last_name: string @template("Surname") 15 | age?: int @template(21) 16 | company?: string @template("CueBlox") 17 | title?: string @template("Cue Slinger") 18 | body?: string @template("☕️ Required") 19 | image_id?: string 20 | social_accounts?: [...#TwitterAccount | #GitHubAccount | #MiscellaneousAccount] 21 | } 22 | 23 | #TwitterAccount: { 24 | network: "twitter" 25 | username: string @template("twitter-handle") 26 | url: *"https://twitter.com/\(username)" | string 27 | } 28 | 29 | #GitHubAccount: { 30 | network: "github" 31 | username: string @template("github-handle") 32 | url: *"https://github.com/\(username)" | string 33 | } 34 | 35 | #MiscellaneousAccount: { 36 | network: string @template("some_network") 37 | url: string @template("https://some_url") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /dogfood/repository/section/v1/schema.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Section" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Section: { 8 | _dataset: { 9 | plural: "sections" 10 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 11 | } 12 | 13 | name: string @template("Name") 14 | description: string @template("Small description") 15 | image_id?: string 16 | body?: string @template("All about this section") 17 | weight?: int | *0 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /dogfood/repository/website/v1/schema.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Profile" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Website: { 8 | _dataset: { 9 | plural: "websites" 10 | supportedExtensions: ["yaml", "yml"] 11 | } 12 | 13 | url: string @template("https://google.com") 14 | profile_id?: string 15 | body?: string 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dogfood/schemata/article_v1.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Article" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Article: { 8 | _dataset: { 9 | plural: "articles" 10 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 11 | } 12 | 13 | title: string @template("My New Article") 14 | excerpt: string @template("Small Description") 15 | featured: bool | *false 16 | draft: bool | *false 17 | publish_date: string @template("2020-01-01") 18 | image?: string @relationship(Image) 19 | last_edit_date?: string 20 | edit_description?: string 21 | body?: string @template("My Awesome Article") 22 | tags?: [...string] 23 | category?: string @relationship(Category) 24 | profile?: string @relationship(Profile) 25 | 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /dogfood/schemata/category_v1.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Category" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Category: { 8 | _dataset: { 9 | plural: "categories" 10 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 11 | } 12 | 13 | name: string @template("Name") 14 | description: string @template("Description") 15 | image?: string 16 | body?: string @template("This is my category for ...") 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /dogfood/schemata/image_v1.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Image" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Image: { 8 | _dataset: { 9 | plural: "images" 10 | supportedExtensions: ["yaml", "yml"] 11 | } 12 | 13 | file_name: string 14 | width: int 15 | height: int 16 | alt_text?: string 17 | caption?: string 18 | attribution?: string 19 | attribution_link?: string 20 | cdn?: string 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /dogfood/schemata/page_v1.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Page" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Page: { 8 | _dataset: { 9 | plural: "pages" 10 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 11 | } 12 | 13 | title: string @template("My New Page") 14 | excerpt: string @template("Small description about my page") 15 | draft: bool | *false 16 | publish_date: string @template("2020-01-01") 17 | image?: string 18 | body?: string 19 | tags?: [...string] 20 | section?: string 21 | weight?: int 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /dogfood/schemata/profile_v1.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Profile" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Profile: { 8 | _dataset: { 9 | plural: "profiles" 10 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 11 | } 12 | 13 | first_name: string @template("Forename") 14 | last_name: string @template("Surname") 15 | age?: int @template(21) 16 | company?: string @template("CueBlox") 17 | title?: string @template("Cue Slinger") 18 | body?: string @template("☕️ Required") 19 | image?: string 20 | // social_accounts?: [...#TwitterAccount | #GitHubAccount | #MiscellaneousAccount] 21 | } 22 | 23 | #TwitterAccount: { 24 | network: "twitter" 25 | username: string @template("twitter-handle") 26 | url: *"https://twitter.com/\(username)" | string 27 | } 28 | 29 | #GitHubAccount: { 30 | network: "github" 31 | username: string @template("github-handle") 32 | url: *"https://github.com/\(username)" | string 33 | } 34 | 35 | #MiscellaneousAccount: { 36 | network: string @template("some_network") 37 | url: string @template("https://some_url") 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /dogfood/schemata/section_v1.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Section" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Section: { 8 | _dataset: { 9 | plural: "sections" 10 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 11 | } 12 | 13 | name: string @template("Name") 14 | description: string @template("Small description") 15 | body?: string @template("All about this section") 16 | weight?: int | *0 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /dogfood/schemata/website_v1.cue: -------------------------------------------------------------------------------- 1 | { 2 | _schema: { 3 | name: "Website" 4 | namespace: "schemas.cueblox.com" 5 | } 6 | 7 | #Website: { 8 | _dataset: { 9 | plural: "websites" 10 | supportedExtensions: ["yaml", "yml"] 11 | } 12 | 13 | url: string @template("https://google.com") 14 | profile?: string @relationship(Profile) 15 | body?: string 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/api/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/api/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # TypeScript output 87 | dist 88 | out 89 | 90 | # Azure Functions artifacts 91 | bin 92 | obj 93 | appsettings.json 94 | local.settings.json -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/api/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-azuretools.vscode-azurefunctions" 4 | ] 5 | } -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/api/DataApi/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": [ 9 | "get", 10 | "post" 11 | ], 12 | "route": "{*segments}" 13 | }, 14 | { 15 | "type": "http", 16 | "direction": "out", 17 | "name": "res" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/api/DataApi/index.js: -------------------------------------------------------------------------------- 1 | const fetch = require('sync-fetch'); 2 | 3 | const jsonGraphqlExpress = require('json-graphql-server').default; 4 | const createHandler = require("azure-function-express").createHandler; 5 | const jsonServer = require('json-server') 6 | 7 | 8 | const data = fetch('https://github.com/cueblox/blox/releases/download/blox/data.json').json(); 9 | 10 | const router = jsonServer.router(data, { foreignKeySuffix: '_id' }) 11 | const app = require('express')(); 12 | 13 | 14 | app.use('/api/graphql', jsonGraphqlExpress(data)); 15 | app.use("/api", router); 16 | 17 | console.log(data); 18 | 19 | module.exports = createHandler(app); 20 | -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/api/DataApi/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/api/GraphQL/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "route": "graphql" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/api/GraphQL/index.js: -------------------------------------------------------------------------------- 1 | const fetch = require('sync-fetch'); 2 | 3 | const jsonGraphqlExpress = require('json-graphql-server').default; 4 | const createHandler = require("azure-function-express").createHandler; 5 | const jsonServer = require('json-server') 6 | const router = jsonServer.router(data, { foreignKeySuffix: '_id' }) 7 | 8 | 9 | const data = fetch('https://github.com/cueblox/blox/releases/download/blox/data.json').json(); 10 | const app = require('express')(); 11 | 12 | 13 | app.use('/api/graphql', jsonGraphqlExpress(data)); 14 | app.use("/api", router); 15 | 16 | console.log(data); 17 | 18 | module.exports = createHandler(app); 19 | -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/api/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[1.*, 2.0.0)" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "func start", 7 | "test": "echo \"No tests yet...\"" 8 | }, 9 | "dependencies": { 10 | "azure-function-express": "^2.0.0", 11 | "express": "^4.17.1", 12 | "glob": "^7.1.6", 13 | "json-graphql-server": "^2.2.0", 14 | "json-server": "^0.16.3", 15 | "sync-fetch": "0.3.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/api/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Content Repository 9 | 10 | 11 | 12 |
13 |

CueBlox Repository

14 | Live Query 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /dogfood/sites/api.cueblox.com/app/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: Arial, Helvetica, sans-serif; 3 | } 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | border: 0; 9 | padding: 0; 10 | background-color: #fff; 11 | } 12 | 13 | main { 14 | margin: auto; 15 | width: 50%; 16 | padding: 20px; 17 | } 18 | 19 | main > h1 { 20 | text-align: center; 21 | font-size: 3.5em; 22 | } 23 | -------------------------------------------------------------------------------- /dogfood/sites/docs/.gitignore: -------------------------------------------------------------------------------- 1 | site/ -------------------------------------------------------------------------------- /dogfood/sites/docs/README.md: -------------------------------------------------------------------------------- 1 | # CueBlox Docs 2 | 3 | ## Developing 4 | 5 | ```shell 6 | # If you don't have mkdocs installed, you can use 7 | poetry install --no-root 8 | 9 | # Run mkdocs locally 10 | mkdocs serve 11 | ``` 12 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox.md: -------------------------------------------------------------------------------- 1 | # blox 2 | 3 | CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents. 4 | 5 | ## Options 6 | 7 | ``` 8 | --debug enable debug logging, overrides 'quiet' flag 9 | -h, --help help for blox 10 | --quiet disable logging 11 | ``` 12 | 13 | ## See also 14 | 15 | * [blox build](/cmd/blox_build) - Validate & Build dataset 16 | * [blox completion](/cmd/blox_completion) - Prints shell autocompletion scripts for blox 17 | * [blox init](/cmd/blox_init) - Create folders and configuration to maintain content with the blox toolset 18 | * [blox new](/cmd/blox_new) - Create a new content file for the target dataset 19 | * [blox remote](/cmd/blox_remote) - Manage Schemata 20 | * [blox render](/cmd/blox_render) - Render templates with compiled data 21 | * [blox repo](/cmd/blox_repo) - Create & Manage Schema Repositories 22 | * [blox schema](/cmd/blox_schema) - Create, Manage, and Version your Schemata 23 | * [blox serve](/cmd/blox_serve) - Serve a GraphQL API 24 | 25 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_build.md: -------------------------------------------------------------------------------- 1 | # blox build 2 | 3 | Validate & Build dataset 4 | 5 | ## Synopsis 6 | 7 | The build command will ensure that your dataset is correct by 8 | validating it against your schemata. Once validated, it will render all 9 | your content into a single JSON file, which can be consumed by your tooling 10 | of choice. 11 | 12 | Referential Integrity can be enforced with -i. This ensures that any fields 13 | ending with _id are valid references to identifiers within the other content type. 14 | 15 | 16 | ``` 17 | blox build [flags] 18 | ``` 19 | 20 | ## Options 21 | 22 | ``` 23 | -h, --help help for build 24 | -i, --referential-integrity Verify referential integrity 25 | ``` 26 | 27 | ## Options inherited from parent commands 28 | 29 | ``` 30 | --debug enable debug logging, overrides 'quiet' flag 31 | --quiet disable logging 32 | ``` 33 | 34 | ## See also 35 | 36 | * [blox](/cmd/blox) - CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents. 37 | 38 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_completion.md: -------------------------------------------------------------------------------- 1 | # blox completion 2 | 3 | Prints shell autocompletion scripts for blox 4 | 5 | ## Synopsis 6 | 7 | Allows you to setup your shell to completions blox commands and flags. 8 | 9 | ### Bash 10 | 11 | $ source <(blox completion bash) 12 | 13 | To load completions for each session, execute once: 14 | 15 | #### Linux 16 | 17 | $ blox completion bash > /etc/bash_completion.d/blox 18 | 19 | #### MacOS 20 | 21 | $ blox completion bash > /usr/local/etc/bash_completion.d/blox 22 | 23 | ### ZSH 24 | 25 | If shell completion is not already enabled in your environment you will need to enable it. 26 | You can execute the following once: 27 | 28 | $ echo "autoload -U compinit; compinit" >> ~/.zshrc 29 | 30 | To load completions for each session, execute once: 31 | 32 | $ blox completion zsh > "${fpath[1]}/_blox" 33 | 34 | You will need to start a new shell for this setup to take effect. 35 | 36 | ### Fish 37 | 38 | $ blox completion fish | source 39 | 40 | To load completions for each session, execute once: 41 | 42 | $ blox completion fish > ~/.config/fish/completions/blox.fish 43 | 44 | **NOTE**: If you are using an official blox package, it should setup completions for you out of the box. 45 | 46 | 47 | ``` 48 | blox completion [bash|zsh|fish] 49 | ``` 50 | 51 | ## Options 52 | 53 | ``` 54 | -h, --help help for completion 55 | ``` 56 | 57 | ## Options inherited from parent commands 58 | 59 | ``` 60 | --debug enable debug logging, overrides 'quiet' flag 61 | --quiet disable logging 62 | ``` 63 | 64 | ## See also 65 | 66 | * [blox](/cmd/blox) - CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents. 67 | 68 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_init.md: -------------------------------------------------------------------------------- 1 | # blox init 2 | 3 | Create folders and configuration to maintain content with the blox toolset 4 | 5 | ## Synopsis 6 | 7 | Create a group of folders to store your content. A directory for your data, 8 | schemata, and build output will be created. 9 | 10 | ``` 11 | blox init [flags] 12 | ``` 13 | 14 | ## Options 15 | 16 | ``` 17 | -b, --build string where post-processed content will be stored (output json) (default "_build") 18 | -d, --data string where pre-processed content will be stored (source markdown or yaml) (default "data") 19 | -h, --help help for init 20 | -s, --schemata string where the schemata will be stored (default "schemata") 21 | -c, --skip don't write a configuration file 22 | -t, --starter string use a pre-defined starter in the CURRENT directory 23 | -a, --static string where the static originals will be found (default "static") 24 | ``` 25 | 26 | ## Options inherited from parent commands 27 | 28 | ``` 29 | --debug enable debug logging, overrides 'quiet' flag 30 | --quiet disable logging 31 | ``` 32 | 33 | ## See also 34 | 35 | * [blox](/cmd/blox) - CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents. 36 | 37 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_new.md: -------------------------------------------------------------------------------- 1 | # blox new 2 | 3 | Create a new content file for the target dataset 4 | 5 | ## Synopsis 6 | 7 | This command will allow you to create new content based on the 8 | template attributes within the schemata. By providing a dataset name and ID(slug) 9 | for the new content, you can quickly scaffold new content with ease. 10 | 11 | ``` 12 | blox new [flags] 13 | ``` 14 | 15 | ## Options 16 | 17 | ``` 18 | --dataset string Which DataSet to create content for? 19 | -h, --help help for new 20 | ``` 21 | 22 | ## Options inherited from parent commands 23 | 24 | ``` 25 | --debug enable debug logging, overrides 'quiet' flag 26 | --quiet disable logging 27 | ``` 28 | 29 | ## See also 30 | 31 | * [blox](/cmd/blox) - CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents. 32 | 33 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_remote.md: -------------------------------------------------------------------------------- 1 | # blox remote 2 | 3 | Manage Schemata 4 | 5 | ## Synopsis 6 | 7 | Blox allows you to consume schemata from remote repositories. 8 | The remote subcommands allow you to list the available schemata from these 9 | repositories, as well as download a schema to your local directories. 10 | 11 | ## Options 12 | 13 | ``` 14 | -h, --help help for remote 15 | ``` 16 | 17 | ## Options inherited from parent commands 18 | 19 | ``` 20 | --debug enable debug logging, overrides 'quiet' flag 21 | --quiet disable logging 22 | ``` 23 | 24 | ## See also 25 | 26 | * [blox](/cmd/blox) - CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents. 27 | * [blox remote get](/cmd/blox_remote_get) - Add a remote schema to your repository 28 | * [blox remote list](/cmd/blox_remote_list) - List schemata and versions available in a remote repository 29 | 30 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_remote_get.md: -------------------------------------------------------------------------------- 1 | # blox remote get 2 | 3 | Add a remote schema to your repository 4 | 5 | ``` 6 | blox remote get [flags] 7 | ``` 8 | 9 | ## Options 10 | 11 | ``` 12 | -h, --help help for get 13 | ``` 14 | 15 | ## Options inherited from parent commands 16 | 17 | ``` 18 | --debug enable debug logging, overrides 'quiet' flag 19 | --quiet disable logging 20 | ``` 21 | 22 | ## See also 23 | 24 | * [blox remote](/cmd/blox_remote) - Manage Schemata 25 | 26 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_remote_list.md: -------------------------------------------------------------------------------- 1 | # blox remote list 2 | 3 | List schemata and versions available in a remote repository 4 | 5 | ``` 6 | blox remote list [flags] 7 | ``` 8 | 9 | ## Options 10 | 11 | ``` 12 | -h, --help help for list 13 | ``` 14 | 15 | ## Options inherited from parent commands 16 | 17 | ``` 18 | --debug enable debug logging, overrides 'quiet' flag 19 | --quiet disable logging 20 | ``` 21 | 22 | ## See also 23 | 24 | * [blox remote](/cmd/blox_remote) - Manage Schemata 25 | 26 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_render.md: -------------------------------------------------------------------------------- 1 | # blox render 2 | 3 | Render templates with compiled data 4 | 5 | ## Synopsis 6 | 7 | Render templates with compiled data. 8 | Use the 'with' parameter to restrict the data set to a single content type. 9 | Use the 'each' parameter to execute the template once for each item. 10 | 11 | ``` 12 | blox render [flags] 13 | ``` 14 | 15 | ## Options 16 | 17 | ``` 18 | -e, --each render template once per item 19 | -h, --help help for render 20 | -t, --template string template to render 21 | -w, --with string dataset to use 22 | ``` 23 | 24 | ## Options inherited from parent commands 25 | 26 | ``` 27 | --debug enable debug logging, overrides 'quiet' flag 28 | --quiet disable logging 29 | ``` 30 | 31 | ## See also 32 | 33 | * [blox](/cmd/blox) - CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents. 34 | 35 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_repo.md: -------------------------------------------------------------------------------- 1 | # blox repo 2 | 3 | Create & Manage Schema Repositories 4 | 5 | ## Options 6 | 7 | ``` 8 | -h, --help help for repo 9 | ``` 10 | 11 | ## Options inherited from parent commands 12 | 13 | ``` 14 | --debug enable debug logging, overrides 'quiet' flag 15 | --quiet disable logging 16 | ``` 17 | 18 | ## See also 19 | 20 | * [blox](/cmd/blox) - CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents. 21 | * [blox repo build](/cmd/blox_repo_build) - Build a Schema Repository 22 | * [blox repo init](/cmd/blox_repo_init) - Initialize a New Schema Repository 23 | 24 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_repo_build.md: -------------------------------------------------------------------------------- 1 | # blox repo build 2 | 3 | Build a Schema Repository 4 | 5 | ## Synopsis 6 | 7 | In order to consume your schema repository with the Blox CLI, you 8 | need to build a manifest file and publish. This command provides the build output 9 | that can be deployed to any static file hosting, or even GitHub raw content links. 10 | 11 | ``` 12 | blox repo build [flags] 13 | ``` 14 | 15 | ## Options 16 | 17 | ``` 18 | -h, --help help for build 19 | ``` 20 | 21 | ## Options inherited from parent commands 22 | 23 | ``` 24 | --debug enable debug logging, overrides 'quiet' flag 25 | --quiet disable logging 26 | ``` 27 | 28 | ## See also 29 | 30 | * [blox repo](/cmd/blox_repo) - Create & Manage Schema Repositories 31 | 32 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_repo_init.md: -------------------------------------------------------------------------------- 1 | # blox repo init 2 | 3 | Initialize a New Schema Repository 4 | 5 | ## Synopsis 6 | 7 | Initializing a new schema repository creates the 8 | configuration required to published your schemata. 9 | 10 | ``` 11 | blox repo init [flags] 12 | ``` 13 | 14 | ## Options 15 | 16 | ``` 17 | -h, --help help for init 18 | -n, --namespace string repository namespace (default "schemas.you.com") 19 | -o, --output string directory where build output will be written (default "_build") 20 | -r, --root string directory to store the repository, relative to current directory (default "repository") 21 | ``` 22 | 23 | ## Options inherited from parent commands 24 | 25 | ``` 26 | --debug enable debug logging, overrides 'quiet' flag 27 | --quiet disable logging 28 | ``` 29 | 30 | ## See also 31 | 32 | * [blox repo](/cmd/blox_repo) - Create & Manage Schema Repositories 33 | 34 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_schema.md: -------------------------------------------------------------------------------- 1 | # blox schema 2 | 3 | Create, Manage, and Version your Schemata 4 | 5 | ## Options 6 | 7 | ``` 8 | -h, --help help for schema 9 | ``` 10 | 11 | ## Options inherited from parent commands 12 | 13 | ``` 14 | --debug enable debug logging, overrides 'quiet' flag 15 | --quiet disable logging 16 | ``` 17 | 18 | ## See also 19 | 20 | * [blox](/cmd/blox) - CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents. 21 | * [blox schema list](/cmd/blox_schema_list) - List Schemata 22 | * [blox schema new](/cmd/blox_schema_new) - Create a New Schema 23 | * [blox schema version](/cmd/blox_schema_version) - Schema Version Management 24 | 25 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_schema_list.md: -------------------------------------------------------------------------------- 1 | # blox schema list 2 | 3 | List Schemata 4 | 5 | ## Synopsis 6 | 7 | List schemata that are published via this repository 8 | 9 | ``` 10 | blox schema list [flags] 11 | ``` 12 | 13 | ## Options 14 | 15 | ``` 16 | -h, --help help for list 17 | ``` 18 | 19 | ## Options inherited from parent commands 20 | 21 | ``` 22 | --debug enable debug logging, overrides 'quiet' flag 23 | --quiet disable logging 24 | ``` 25 | 26 | ## See also 27 | 28 | * [blox schema](/cmd/blox_schema) - Create, Manage, and Version your Schemata 29 | 30 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_schema_new.md: -------------------------------------------------------------------------------- 1 | # blox schema new 2 | 3 | Create a New Schema 4 | 5 | ## Synopsis 6 | 7 | Create a new schema that can be published with the repository management commands 8 | 9 | ``` 10 | blox schema new [schema name] [flags] 11 | ``` 12 | 13 | ## Options 14 | 15 | ``` 16 | -h, --help help for new 17 | ``` 18 | 19 | ## Options inherited from parent commands 20 | 21 | ``` 22 | --debug enable debug logging, overrides 'quiet' flag 23 | --quiet disable logging 24 | ``` 25 | 26 | ## See also 27 | 28 | * [blox schema](/cmd/blox_schema) - Create, Manage, and Version your Schemata 29 | 30 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_schema_version.md: -------------------------------------------------------------------------------- 1 | # blox schema version 2 | 3 | Schema Version Management 4 | 5 | ## Options 6 | 7 | ``` 8 | -h, --help help for version 9 | ``` 10 | 11 | ## Options inherited from parent commands 12 | 13 | ``` 14 | --debug enable debug logging, overrides 'quiet' flag 15 | --quiet disable logging 16 | ``` 17 | 18 | ## See also 19 | 20 | * [blox schema](/cmd/blox_schema) - Create, Manage, and Version your Schemata 21 | * [blox schema version add](/cmd/blox_schema_version_add) - Create a New Version of a Schema 22 | 23 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_schema_version_add.md: -------------------------------------------------------------------------------- 1 | # blox schema version add 2 | 3 | Create a New Version of a Schema 4 | 5 | ``` 6 | blox schema version add [schema name] [flags] 7 | ``` 8 | 9 | ## Options 10 | 11 | ``` 12 | -h, --help help for add 13 | ``` 14 | 15 | ## Options inherited from parent commands 16 | 17 | ``` 18 | --debug enable debug logging, overrides 'quiet' flag 19 | --quiet disable logging 20 | ``` 21 | 22 | ## See also 23 | 24 | * [blox schema version](/cmd/blox_schema_version) - Schema Version Management 25 | 26 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/cmd/blox_serve.md: -------------------------------------------------------------------------------- 1 | # blox serve 2 | 3 | Serve a GraphQL API 4 | 5 | ``` 6 | blox serve [flags] 7 | ``` 8 | 9 | ## Options 10 | 11 | ``` 12 | -a, --address string Listen address (default ":8080") 13 | -h, --help help for serve 14 | -s, --static Serve static files (default true) 15 | ``` 16 | 17 | ## Options inherited from parent commands 18 | 19 | ``` 20 | --debug enable debug logging, overrides 'quiet' flag 21 | --quiet disable logging 22 | ``` 23 | 24 | ## See also 25 | 26 | * [blox](/cmd/blox) - CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents. 27 | 28 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/conversion.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Conversion 3 | excerpt: Data Validation 4 | publish_date: '2021-03-19' 5 | section_id: getting-started 6 | weight: 1 7 | --- 8 | 9 | # Conversion 10 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/creating-your-blox/first-data.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Creating Data 3 | publish_date: '2021-03-19' 4 | section_id: getting-started 5 | weight: 1 6 | --- 7 | 8 | # Creating Data 9 | 10 | Continuing from the previous section, we now want to add some data to our blox that conforms to the required schema. Lets remind ourselves of the schema, trimmed of metadata for brevity: 11 | 12 | ```cue 13 | # schemata/artist.cue 14 | { 15 | _schema: { ... } 16 | 17 | #Artist: { 18 | _dataset: { 19 | plural: "artists" 20 | supportedExtensions: ["yaml", "yml", "md"] 21 | } 22 | 23 | name: string 24 | url?: string 25 | hungry: bool | *false 26 | } 27 | } 28 | ``` 29 | 30 | ## Manually Creating Data 31 | 32 | The `plural` field from our DataSet metadata tells us which directory our data, artists, needs to be in to be validating correctly against our schema. 33 | 34 | Blox supports both YAML and Markdown (with frontmatter) content, and we'd welcome other loaders for more. We can constrain the filetypes we want to load for this particular DataSet through `supportedExtensions`. 35 | 36 | This means it's really trivial to add new data to be validated. In this instance, we create some YAML or Markdown files in the `artists` directory. 37 | 38 | ```shell 39 | # linkin-park.yml 40 | name: Linkin Park 41 | url: https://linkinpark.com 42 | 43 | # fleetwood-mac.md 44 | --- 45 | name: Fleetwood Mac 46 | url: https://fleetwoodmac.com 47 | --- 48 | 49 | Fleetwood Mac are a British-American rock band, formed in London in 1967. 50 | ``` 51 | 52 | ### The "Body" Field 53 | 54 | When using markdown files with YAML frontmatter and a body, CueBlox does some "magic" translation. Lets use the Fleetwood Mac example above. This actually translate, in YAML, to: 55 | 56 | ```yaml 57 | name: Fleetwood Mac 58 | url: https://fleetwoodmac.com 59 | body: | 60 | Fleetwood Mac are a British-American rock band, formed in London in 1967. 61 | ``` 62 | 63 | This will actually **fail** validation with our current schema, as `body` isn't a valid property on `#Artist`. So remember to include a `body` field when using this data format. 64 | 65 | ## Using `blox` to Create Data 66 | 67 | CueBlox's CLI, `blox`, also ships with a helper to scaffold new data. 68 | 69 | ```shell 70 | ❯ blox new 71 | Error: requires at least 1 arg(s), only received 0 72 | Usage: 73 | blox new [flags] 74 | 75 | Flags: 76 | --dataset string Which DataSet to create content for? 77 | -h, --help help for new 78 | ``` 79 | 80 | Using `blox new --dataset artist soilwork`, we can have CueBlox create a new data file for us. 81 | 82 | Note: Currently, this only supports writing YAML. PR's welcome. 83 | 84 | The `blox` command works by leveraging `@template` annotations within the schema. Let's update our schema to take advantage of this awesome-sauce. 85 | 86 | ```cue 87 | # schemata/artist.cue 88 | { 89 | _schema: { ... } 90 | 91 | #Artist: { 92 | _dataset: { ... } 93 | 94 | name: string @template(Chevelle) 95 | url?: string @template(https://google.com) 96 | hungry: bool | *false 97 | } 98 | } 99 | ``` 100 | 101 | Now, using `blox new --dataset artist soilwork`, we'll get a data file at `./artists/soilwork.yaml` that looks like: 102 | 103 | ``` 104 | # soilwork.yaml 105 | name: Chevelle 106 | url: https://google.com 107 | hungry: false 108 | ``` 109 | 110 | This is early stage for templates, but we're going to be adding more awesome soon; including an interactive mode to accept user input for fields. 111 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/creating-your-blox/first-schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Defining Your First Schema 3 | publish_date: '2021-03-19' 4 | section_id: getting-started 5 | weight: 1 6 | --- 7 | 8 | # Defining Your First Schema 9 | 10 | Now that we have our configuration and a directory structure, we need to provide a schema to validate any data we add to the data directory. 11 | 12 | Schemata is written in Cue, which comes with a little learning curve; but once you're comfortable with it, it's extremely powerful. 13 | 14 | ## Schema Metadata 15 | 16 | Lets add our first schema to the `schemata` directory by creating a new file called `artist.cue`. 17 | 18 | ```cue 19 | # schemata/artist.cue 20 | { 21 | } 22 | ``` 23 | 24 | In order for the schema to be loaded correctly by CueBlox, we need to provide a little boilerplate that helps identify how to work with the schema. 25 | 26 | ```cue 27 | # schemata/artist.cue 28 | { 29 | _schema: { 30 | name: "Artist" 31 | namespace: "schemas.cueblox.com" 32 | } 33 | ``` 34 | 35 | This metadata provides enough information for CueBlox to begin scanning your schema file for DataSets. 36 | 37 | ## DataSet Metadata 38 | 39 | DataSets also need some metadata. 40 | 41 | ```cue 42 | # schemata/artist.cue 43 | { 44 | # Same as above, omitted for brevity 45 | _schema: { ... } 46 | 47 | #Artist: { 48 | _dataset: { 49 | plural: "artists" 50 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | The `#Artist` key is called a "Definition" in Cue. Definitions are structures that define constraints for the data associated with them. You'll recognise definitions in Cue by the `#` prefix. 57 | 58 | The `plural` value is rather important, as this is the name of a directory within your data directory that contains the data to be loaded and validated against this DataSet. 59 | 60 | The `supportedExtensions` key allows you to provide a list of file extensions to attempt and load the data from. YAML and Markdown are supported at this time. 61 | 62 | ## DataSet Constraints 63 | 64 | That's all the boilerplate. Now we can define what our `Artist` structure should look like. This definition will be used to validate all data within the `data/artists` directory. 65 | 66 | ```cue 67 | # schemata/artist.cue 68 | { 69 | # Same as above, omitted for brevity 70 | _schema: { ... } 71 | 72 | #Artist: { 73 | # Same as above, omitted for brevity 74 | _dataset: { ... } 75 | 76 | name: string 77 | url?: string 78 | 79 | hungry: bool | *false 80 | } 81 | } 82 | ``` 83 | 84 | We've now added our first three fields / property to the Artist definition. Firstly, we've added a mandatory field called `name` and an optional field called `url`. Fields with `?` are optional and can be missing from our data files. The last field, `hungry`, is another CUE construct, this time for default fields. This allows you to have mandatory fields with a sensible default when they're missing. In this instance, we're setting `hungry` to be of type `bool` with a default of `false`. `*` is used to indicate a default value. 85 | 86 | ### Further Reading 87 | 88 | We won't go into more details of CUE itself, but we will encourage you to read the following to understand the full potential of using CUE for validation: 89 | 90 | - [Official Docs](https://cuelang.org/docs/usecases/validation/) 91 | - [CUEtorials](https://cuetorials.com/) 92 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/creating-your-blox/init.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Creating Your Blox 3 | publish_date: '2021-03-19' 4 | section_id: getting-started 5 | weight: 1 6 | --- 7 | 8 | # Creating Your Blox 9 | 10 | CueBlox needs a configuration file and some directories to start validating and transforming your data. You can have CueBlox create this structure for you with `blox init`. 11 | 12 | ```shell 13 | blox init 14 | INFO Initialized folder structures. 15 | ``` 16 | 17 | This creates a `blox.cue` file with the default configuration. 18 | 19 | ```shell 20 | cat blox.cue 21 | { 22 | build_dir: "_build" 23 | data_dir: "data" 24 | schemata_dir: "schemata" 25 | static_dir: "static" 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/custom_schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom Schemata 3 | excerpt: Data Validation 4 | publish_date: '2021-03-19' 5 | section_id: getting-started 6 | weight: 1 7 | --- 8 | 9 | # Custom Schemata 10 | 11 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/extras.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Extras 3 | excerpt: Get up and running quickly 4 | publish_date: '2021-03-19' 5 | section_id: getting-started 6 | weight: 1 7 | --- 8 | 9 | # Extras -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/foundations.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Foundations 3 | excerpt: Fundamentals of CueBlox 4 | publish_date: "2021-03-19" 5 | section_id: foundations 6 | weight: 2 7 | --- 8 | 9 | # Definitions 10 | 11 | Let's start by getting on the same page about what the words in these documents mean. Naming is hard! 12 | 13 | ## Dataset 14 | 15 | A `dataset` is a group of related documents that share the same properties. 16 | The `dataset` that powers this documentation website is called `pages`. Every document in 17 | the `pages` `dataset` has the same metadata. 18 | 19 | ## Metadata 20 | 21 | Every document stores two different types of information: 22 | 23 | * Content 24 | * Information about the content 25 | 26 | The content of the document is the data, the words, the information that you create 27 | to be consumed by an application like a website. You're reading the content of a document called 28 | `foundations.mdx`. 29 | 30 | >Because much of the data that you create will be consumed by a web 31 | >server, we'll often use the term `body` to refer to the content as well. Consider the 32 | >two terms interchangeable. 33 | 34 | In addition to the content stored in this document, there is additional data about the content. 35 | Data about the data is called `metadata`. 36 | 37 | CueBlox can parse documents stored in two different formats: [yaml](https://yaml.org/) and [markdown](https://daringfireball.net/projects/markdown/). 38 | 39 | 40 | `foundations.mdx` is stored in the `mdx` format 41 | which is an enhanced version of markdown that allows the author to include React components. 42 | 43 | ### YAML 44 | YAML documents are parsed by reading key/value pairs. A simple YAML document might look like this: 45 | 46 | ```yaml 47 | url: https://www.cueblox.com 48 | ``` 49 | 50 | ### Markdown 51 | 52 | Markdown documents provide a simplified syntax to format a document. 53 | You can add metadata to a Markdown document by adding a YAML to the top of the Markdown 54 | file. 55 | 56 | ```markdown 57 | --- 58 | title: Foundations 59 | excerpt: Fundamentals of CueBlox 60 | publish_date: "2021-03-19" 61 | section_id: foundations 62 | weight: 2 63 | --- 64 | 65 | # Definitions 66 | 67 | Let's start by getting on the same page about what the words in these documents mean. Naming is hard! 68 | ``` 69 | The YAML added to the markdown document that defines the page you are reading is separated 70 | from the body by enclosing it in three dash characters: ```---```. When a markdown document 71 | includes metadata in this form, it's called `FrontMatter`. 72 | 73 | Whether your document is stored in YAML format, or markdown with YAML frontmatter, we refer to 74 | the information about the document as `metadata`. 75 | 76 | ## Schema 77 | 78 | In order to enforce consistency in a dataset, CueBlox uses a `schema`, which is a set of 79 | rules, defaults, and metadata about a dataset. We use `schemata` as the plural of `schema`. 80 | 81 | The `schema` is defined using [cue](https://cuelang.org), but you don't need to become an 82 | expert in Cue to start making your own `schema`. 83 | 84 | ### Example Schema 85 | Here's the schema that defines the `page` dataset that powers the documention you're reading. 86 | 87 | ```json 88 | { 89 | _schema: { 90 | name: "Page" 91 | namespace: "schemas.cueblox.com" 92 | } 93 | 94 | #Page: { 95 | _dataset: { 96 | plural: "pages" 97 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 98 | } 99 | 100 | title: string @template("My New Page") 101 | excerpt: string @template("Small description about my page") 102 | draft: bool | *false 103 | publish_date: string @template("2020-01-01") 104 | image?: string 105 | body?: string 106 | tags?: [...string] 107 | section_id?: string 108 | weight?: int 109 | } 110 | 111 | } 112 | ``` 113 | 114 | It starts with some metadata about the schema in a field called (appropriately) `_schema`. 115 | The `_schema` field defines the name and namespace of the `schema`. This allows CueBlox to 116 | find the right `schema` to validate and process your documents. 117 | 118 | The next section in the `schema` defines a `Page`. It contains more metadata in the `_dataset` field 119 | which we use to find your content. 120 | 121 | The most important part of the `Page` definition is the list of fields that are defined in a `Page` 122 | document. Each field is defined with a name, a data type, and other optional metadata about 123 | the field, such as defaults. 124 | 125 | Fields that are optional have a `?` at the end of the field name. 126 | 127 | Fields definitions can provide a default value by specifying it after the data type: 128 | ```json 129 | draft: bool | *false 130 | ``` 131 | 132 | The most common data types you will use are `string`, `int`, and `bool`. You can find all 133 | the supported data types in the documentation on [cuelang.org](https://cuelang.org) 134 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/img/pyramid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cueblox/blox/8416baa456e0a05689d152367b4e70266f434c33/dogfood/sites/docs/docs/img/pyramid.png -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | # What is CueBlox? 3 | 4 | CueBlox is a set of tools that allow you to create and consume datasets from YAML or Markdown files. 5 | 6 | At the core is a tool that aggregates similar files into collections of data. If you've ever built a website with a static site generator like Hugo, this will be familiar. 7 | 8 | Where CueBlox really shines though is in the additional functionality it enables. CueBlox has several features that enable some interesting and novel integrations for your data. You can use one or all of these features, depending on your needs. We like to think of it as a "Value Pyramid", because the more CueBlox features you use, the more of its value you realize: 9 | 10 | ![CueBlox Value Pyramid](img/pyramid.png "CueBlox Value Pyramid") 11 | 12 | 13 | ## Markdown and YAML Validation 14 | 15 | Ensure your data is always valid by providing defaults and validation rules using the [Cue](https://cuelang.org) language to define schemata for your data. 16 | 17 | The schema that validates this page looks like the code block below. We've commented it pretty heavily to introduce you to Cue. 18 | 19 | ```json 20 | // Anything prefixed with '#' in Cue is called a "Definition". Think of it like a struct. 21 | #Page: { 22 | // Anything prefixed with '_' is "private". This will not be exported 23 | // when converting this Cue to another format. 24 | _dataset: { 25 | // "pages" is a concrete value. It cannot be modified later 26 | plural: "pages" 27 | 28 | // Cue supports lists, much like YAML. This is also concrete 29 | supportedExtensions: ["yaml", "yml", "md", "mdx"] 30 | } 31 | 32 | // These values are open (not concrete), as they're represented as types 33 | title: string 34 | excerpt: string 35 | 36 | // Using `|` is a disjunction and means multiple values are being supported. 37 | // Here we're saying that draft can be a bool "OR" *false, 38 | // where "*" indicates a default value 39 | draft: bool | *false 40 | 41 | publish_date: string 42 | 43 | // "?" means optional. This value doesn't need to exist in the data 44 | // the omission of "?" means required 45 | image?: string 46 | body?: string 47 | 48 | // Tags is a list of strings 49 | // Lists can be a little weird at first, with Cue. 50 | // "[string]" would mean a list with a single string value 51 | // "[string, int]" would mean a list with the first value string, second value int 52 | // "..." here means any number of string values 53 | tags?: [...string] 54 | section_id?: string 55 | weight?: int 56 | } 57 | ``` 58 | 59 | ## Data Conversion 60 | 61 | Data that is locked in your git repository is only useful in that repository. CueBlox allows you to validate, aggregate, and export your data in the integration-friendly JSON format for consumption elsewhere. The data that drives this website is processed through CueBlox and automatically published as a GitHub release. You can see it [here](https://github.com/cueblox/blox/releases/tag/blox) 62 | 63 | 64 | ## Shared Schemata 65 | 66 | The schemata you create are available to you locally in your content directory. But you can also create a set of schemata that is published for others to consume. The schema we use to build all of the CueBlox website are published on [this website](https://schemas.cueblox.com/) All the information about the schemata available, including version information is available in the `manifest.json` file linked in the HTML. 67 | 68 | The `blox` cli tool allows you to add these remote schemata to your project: 69 | 70 | ``` 71 | ❯ blox remote list schemas.cueblox.com 72 | Namespace | Schema | Version 73 | schemas.cueblox.com | article | v1 74 | schemas.cueblox.com | category | v1 75 | schemas.cueblox.com | page | v1 76 | schemas.cueblox.com | profile | v1 77 | schemas.cueblox.com | section | v1 78 | schemas.cueblox.com | website | v1 79 | 80 | ❯ blox remote get schemas.cueblox.com page v1 81 | ``` 82 | 83 | Using published schemata this way allows you to create and consume standardized data across several projects, teams, and people. In fact, it is the core driver for our development of CueBlox -- enabling teams to publish information individually, but have it aggregated and consumed at a higher level without data validation worries. When everyone uses the same schema, all the data is always consistent. 84 | 85 | ## Custom Schemata Repository 86 | 87 | When you grow beyond the schemata we provide by default you can use the `blox repo` command to create your own schema repository. You'll have full control over the definitions and you can point your team at your custom repository to keep everyone on the same page. 88 | 89 | ## Consistent Aggregated Team Data 90 | 91 | When everyone uses the same schemata you can easily aggregate data from multiple sources with the confidence that there won't be any anomalies or inconsistencies in their data. Manage a team event calendar. Create a publishing schedule for your social media. Aggregate documentation from different projects into a single website. You're only limited by your imagination. 92 | 93 | 94 | ## Leverage all of this functionality using GitOps principles 95 | 96 | CueBlox was built for lazy people. If it can be generated, we generate it. If it can be automated, we automate it. The end result is that the only thing you need to do to publish your content is check it into your git repository. GitHub actions take care of the rest! 97 | 98 | 99 | ## Use third party tools to make your data easily accessible 100 | 101 | CueBlox `build` command provides a JSON file that can be published as a release artifact, that works extremely well with GraphQL and REST based API providers. In fact, this [website is built](https://github.com/cueblox/blox/tree/main/dogfood) on this feature. 102 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | excerpt: What is CueBlox 4 | publish_date: "2021-03-19" 5 | section_id: introduction 6 | weight: 2 7 | --- 8 | 9 | # Introduction 10 | 11 | CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents. 12 | 13 | ## Motivation 14 | 15 | We built CueBlox out of a desire to share data in a group or team setting without worrying about incompatible metadata. Markdown (with YAML frontmatter) and just plain YAML are great tools for content authors, but we were missing a reliable way of ensuring the content created by different people -- possibly in different repositories -- has the same metadata. Agreeing to use a common template for your frontmatter is a good start, but even that falls short when inconsistencies in metadata values creep in. 16 | 17 | With data consistency at the core, our next concern was making the data easily accessible. We explored methods of serving content repositories over GraphQL and REST by trying to build a complicated server that syncs remote git repositories to server temp storage, parses and validates them, then serves the combined data over GraphQL. While technically possible, there were many moving parts and it felt like too much manual work for a task that should be relatively simple. 18 | 19 | Finally, we wanted to make it trivial to make a content repository available for consumption publicly or privately over industry-standard protocols. You shouldn't have to install custom tools to consume data. It should be available in standard encoding formats over standard transfer protocols. 20 | 21 | ## The CueBlox Solution 22 | 23 | CueBlox provides a scaffold system that allows you to create a content repository with well-defined schemata. Schema definitions are stored with the content so that any consumer of the content has the information required to validate and parse the content. 24 | 25 | Schema definitions are written in [Cue](https://cuelang.org), providing a powerful method of defining field-level requirements and providing default values for fields. But don't worry about having to learn yet another language... schema definitions are straightforward and look like JSON. 26 | 27 | CueBlox also supports a convention-based relational data system by automatically linking and validating foreign keys which follow basic naming conventions. A document can reference a document of another type by adding a foreign key field to the metadata. Much like the relational database concepts on which this is based, CueBlox will validate that the record referenced in your foreign key field exists. This allows documents of different schemas to reference relational data in a familiar format without having to specify relationships in configuration files. 28 | 29 | A concrete example of this is the typical blog post. A blog post written in Markdown might have YAML metadata in the frontmatter that describes the post and provides additional information which is used to format and display the post. Here's a the frontmatter for the page you're reading now: 30 | 31 | ```yaml 32 | title: Introduction 33 | excerpt: What is CueBlox 34 | publish_date: "2021-03-19" 35 | section_id: introduction 36 | weight: 2 37 | ``` 38 | 39 | This frontmatter lives in a file called `introduction.md` in the `documentation` folder of our content repository. The `section_id` field is a foreign key reference to a document in the `sections` folder which is named `introduction.md`. In relational database terms, the Introduction page belongs to the Introduction section. `Sections` have many `Pages`, `Pages` belong to `Sections`. When validating the content, CueBlox will ensure that the `Section` referenced actually exists, and throw an error if it doesn't. 40 | 41 | Field-level validations and relational validations ensure that your data exists in the shape you intended, without common errors. Most publishing platforms will happily allow you to have frontmatter fields that are not known to the system consuming them. If you mis-spell `excerpt` in your frontmatter, you're not likely to know it until the publishing system gets the data and there's no summary in the listing page. CueBlox prevents this by giving you the ability to define required and optional fields, and allowing you to "close" a definition, preventing extra fields from being added. 42 | 43 | ## Beyond Validation 44 | 45 | Providing schema and relational validation is the foundation of CueBlox, but it only solves the consistency issue. The next layer of CueBlox takes a content repository and assembles it into a JSON object that can be consumed anywhere. You can assemble your content into JSON locally next the applications that consume it, or you can use other tools to serve that data over the Internet. 46 | 47 | CueBlox provides pre-built examples that allow you to build automated content deployment pipelines hosted on your favorite cloud. We've built easy to integrate GitHub actions to automate validation and deployment, as well. In a matter of minutes you can publish a content repository over GraphQL and REST APIs with automatic deployments on all the major hosting providers. CueBlox was built to be deployed inexpensively, and it's likely that your favorite hosting provider has a free tier of hosting that will be more than sufficient for CueBlox's minimal hosting requirements. 48 | 49 | ## Teamwork Makes the Dream Work 50 | 51 | A fundamental problem we set out to solve was collaboration across teams of any size. CueBlox enables this by allowing teams to create and publish their own Schemata, including built-in support for versioning. With shared schemata, your team can be confident that everyone will create data that can be consumed and aggregated without manual intervention. 52 | 53 | ## Beyond The Blog 54 | 55 | While the toolset is well suited for managing markdown content that you intend to publish, that's far from the only use case we envisioned. One of our primary goals was to allow a git-based workflow for other types of data. As a concrete example, the primary authors of CueBlox are in Developer Relations. We intend to create shared schemata for our teams to allow us to share information about events, speaking engagements, conferences and other types of data that might be aggregated to create a team calendar, or provide metrics for management. Because CueBlox is markdown or YAML stored in a git repository, we can even enable git workflows for approval. Imagine creating a travel request by submitting a PR to a shared team content repository. Approval and merging automatically create the data about the event and travel costs. Your workflows are only limited by your imagination, but CueBlox enables everyone to create and consume the data. 56 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/recipes/github-releases.md: -------------------------------------------------------------------------------- 1 | # Host Your Dataset on GitHub Using Releases 2 | 3 | GitHub Releases allow you to host pre-built assets, source code, etc. for download. This recipe takes advantage of GitHub Releases using a fixed release tag name to build your dataset and make it available at a fixed download URL. 4 | 5 | ## This recipe is the foundation of nearly all the other things we do with cueblox 6 | 7 | ## Prerequisites 8 | 9 | * content repository hosted on GitHub 10 | * blox.cue configuration is complete 11 | 12 | ## Presumptions 13 | 14 | Since your configuration can vary drastically based on your needs, we'll be working with the following assumptions: 15 | 16 | Directory structure: 17 | 18 | ```bash 19 | $ tree -L 1 20 | . 21 | ├── README.md 22 | ├── data 23 | ├── unrelated_directory 24 | └── other_unrelated_directory 25 | ``` 26 | 27 | Our CueBlox managed data lives in the `data` directory, and the configuration file is in that directory as well. 28 | 29 | blox.cue contents: 30 | ```json 31 | { 32 | data_dir: "." 33 | schemata_dir: "schemata" 34 | build_dir: ".build" <-- Take note of this 35 | template_dir: "tpl" 36 | static_dir: "static" 37 | } 38 | ``` 39 | 40 | Important to note: the `data_dir` is set to `.` -- the current directory. This isn't required for this recipe to work, it simply allows our directory structure to be a little more flat. The important piece is `build_dir` which is set to `.build`. These directories are relative to the location of the `blox.cue` file, so in our example, the output from `blox build` will be at `$REPO_ROOT/data/.build/data.json`. 41 | 42 | ## Releasing with GitHub Actions 43 | 44 | Create a new GitHub Action by placing a file in `$REPO_ROOT/.github/workflows`. You can call it anything, if you don't specify a name in the Action's YAML definition, the file name will be used. We will use `data.yaml` in this recipe, to give us a workflow called `data`. 45 | 46 | data.yaml: 47 | 48 | ```yaml 49 | on: 50 | push: 51 | paths: 52 | - .github/** 53 | - data/** <-- run when files change here 54 | 55 | jobs: 56 | build: 57 | runs-on: ubuntu-latest 58 | name: Publish CueBlox 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@v2 62 | 63 | - name: Build & Validate Blox Data 64 | id: build 65 | uses: cueblox/github-action@v0.0.8 66 | with: 67 | directory: data <-- location of blox.cue 68 | 69 | - uses: marvinpinto/action-automatic-releases@latest 70 | with: 71 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 72 | automatic_release_tag: "blox" 73 | prerelease: true 74 | title: "CueBlox" 75 | files: | 76 | data/.build/data.json <-- build_dir + data.json 77 | ``` 78 | 79 | This workflow uses the `cueblox/github-action` action to compile and validate your dataset, passing in the working directory `data` to tell `blox` where to look for your `blox.cue` file. 80 | 81 | The last step uses `marvinpinto/action-automatic-releases` to create a release of your dataset. Because it specifies `prerelease: true` and `automatic_release_tag: "blox"`, the release will always have the tag `blox`, which means it will always be available at the same URL. 82 | 83 | If you follow this pattern your releases will be available at a URL that looks like this: 84 | 85 | ``` 86 | https://github.com/you/reponame/releases/download/blox/data.json 87 | ``` 88 | 89 | Now you have a fixed location to download your dataset which will be updated automatically every time you push new files to your content repository. 90 | 91 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/recipes/graphql.md: -------------------------------------------------------------------------------- 1 | # Serving Data as GraphQL 2 | 3 | ## Prerequisites 4 | 5 | * content repository hosted on GitHub 6 | * blox.cue configuration is complete 7 | * [dataset hosted on GitHub](/recipes/github-releases), or another fixed URL 8 | 9 | ## json-server 10 | 11 | Use the awesome [json-graphql-server](https://www.npmjs.com/package/json-graphql-server) to serve your dataset as GraphQL. 12 | 13 | The implementation will vary based on how you choose to run the service, but we've really enjoyed using `serverless`/Function hosting platforms like Azure Functions, Vercel, AWS Lambda, and Netlify to host this service. Here's the core of the recipe: 14 | 15 | ```javascript 16 | const fetch = require("sync-fetch"); 17 | 18 | const jsonGraphqlExpress = require("json-graphql-server").default 19 | const express = require("express"); 20 | 21 | const data = fetch( 22 | "https://github.com/bketelsen/bkml/releases/download/blox/data.json" 23 | ).json(); 24 | const app = require("express")(); 25 | 26 | 27 | app.use("/api/graphql", jsonGraphqlExpress(data)); 28 | 29 | 30 | const port = process.env.PORT || 3000; 31 | 32 | module.exports = app.listen(port, () => 33 | console.log(`Server running on ${port}, http://localhost:${port}`) 34 | ); 35 | 36 | ``` 37 | 38 | We're using `json-graphql-server` to serve the dataset as an `express` application at the `/api/graphql` route. When you run this, you can view the GraphIQL endpoint with your web browser, or make programmatic requests with a graphql client. 39 | 40 | See the [documentation](https://www.npmjs.com/package/json-graphql-server) for complete details. 41 | 42 | This recipe won't typically work without some modification of the source. The key is to figure out how to adapt an `express` endpoint to your hosting provider's serverless hosting. Vercel is happy to serve up a function running `express` without modification. Azure Functions requires an adapter like [azure-function-express](https://www.npmjs.com/package/azure-function-express). 43 | 44 | 45 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/recipes/index.md: -------------------------------------------------------------------------------- 1 | 2 | # Common Patterns 3 | 4 | CueBlox can be the foundation of an impressive git-based automation workflow. 5 | 6 | The following recipes show some suggestions on ways to consume your CueBlox dataset. 7 | 8 | ## Building and Hosting Datasets 9 | 10 | * [Host dataset on GitHub](/recipes/github-releases) Start HERE! 11 | * [Serve data with REST](/recipes/rest) 12 | * [Serve data over GraphQL](/recipes/graphql) 13 | 14 | 15 | ## Patterns 16 | 17 | * [The MonoRepo](/recipes/monorepo) -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/recipes/monorepo.md: -------------------------------------------------------------------------------- 1 | 2 | # The MonoRepo 3 | 4 | ## Co-Locate Data and Consumers 5 | 6 | See our personal examples: 7 | 8 | * [Brian](https://github.com/bketelsen/bkml) 9 | * [David](https://github.com/rawkode/rawkode) -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/recipes/rest.md: -------------------------------------------------------------------------------- 1 | 2 | # Serving Data over REST 3 | 4 | ## Prerequisites 5 | 6 | * content repository hosted on GitHub 7 | * blox.cue configuration is complete 8 | * [dataset hosted on GitHub](/recipes/github-releases), or another fixed URL 9 | 10 | ## json-server 11 | 12 | Use the awesome [json-server](https://www.npmjs.com/package/json-server) to serve your dataset as a REST API. 13 | 14 | The implementation will vary based on how you choose to run the service, but we've really enjoyed using `serverless`/Function hosting platforms like Azure Functions, Vercel, AWS Lambda, and Netlify to host this service. Here's the core of the recipe: 15 | 16 | ```javascript 17 | const fetch = require("sync-fetch"); 18 | 19 | const jsonServer = require('json-server') 20 | const express = require("express"); 21 | 22 | const data = fetch( 23 | "https://github.com/you/yourrepo/releases/download/blox/data.json" 24 | ).json(); 25 | const app = require("express")(); 26 | const router = jsonServer.router(data, { foreignKeySuffix: '_id' }) 27 | 28 | 29 | app.use("/api", router); 30 | 31 | 32 | const port = process.env.PORT || 3000; 33 | 34 | module.exports = app.listen(port, () => 35 | console.log(`Server running on ${port}, http://localhost:${port}`) 36 | ); 37 | ``` 38 | 39 | We're using `json-server` to serve the dataset as an `express` application at the `/api` route. When you run this, you can make a `GET` request to `/api/datasetname` (`/api/articles` for example) and get back all the data in that dataset. 40 | 41 | See the [documentation](https://www.npmjs.com/package/json-server) for complete details. 42 | 43 | This recipe won't typically work without some modification of the source. The key is to figure out how to adapt an `express` endpoint to your hosting provider's serverless hosting. Vercel is happy to serve up a function running `express` without modification. Azure Functions requires an adapter like [azure-function-express](https://www.npmjs.com/package/azure-function-express). 44 | 45 | 46 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/shared_schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Shared Schemata 3 | excerpt: Data Validation 4 | publish_date: '2021-03-19' 5 | section_id: getting-started 6 | weight: 1 7 | --- 8 | 9 | # Shared Schemata 10 | 11 | -------------------------------------------------------------------------------- /dogfood/sites/docs/docs/validation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Validation 3 | excerpt: Data Validation 4 | publish_date: '2021-03-19' 5 | section_id: getting-started 6 | weight: 1 7 | --- 8 | 9 | # Validation 10 | 11 | Validation is the first thing that CueBlox can bring to your YAML and Markdown content. In a CueBlox root, you can run `blox build` to validate your content against the defined schema. If you don't have a CueBlox project yet, you can create one with `blox init` or use the `cueblox/blox` repository itself. 12 | 13 | Build will validate all of the content and transform it into a JSON file to be consumed by other tools. 14 | 15 | ```shell 16 | # From the cueblox/blox Git clone 17 | blox build 18 | INFO processing images in dogfood/static 19 | SUCCESS Validation Complete 20 | SUCCESS Data blox written to '_build/data.json' 21 | ``` 22 | -------------------------------------------------------------------------------- /dogfood/sites/docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: CueBlox 2 | repo_url: https://github.com/cueblox/blox 3 | nav: 4 | - Home: index.md 5 | - "User Guide": 6 | - "Introduction": introduction.md 7 | - "Quick Start": get-started.md 8 | - "Data Validation": validation.md 9 | - "Data Conversion": conversion.md 10 | - "Shared Schemata": shared_schema.md 11 | - "Custom Schemata": custom_schema.md 12 | - "Walkthrough": 13 | - "Creating Your Blox": creating-your-blox/init.md 14 | - "Defining Your First Schema": creating-your-blox/first-schema.md 15 | - "Creating Data": creating-your-blox/first-data.md 16 | - "CLI": 17 | - "blox": cmd/blox.md 18 | - "blox init": cmd/blox_init.md 19 | - "blox build": cmd/blox_build.md 20 | - "blox new": cmd/blox_new.md 21 | - "blox completion": cmd/blox_completion.md 22 | - "blox render": cmd/blox_render.md 23 | - "blox remote": cmd/blox_remote.md 24 | - "blox remote get": cmd/blox_remote_get.md 25 | - "blox remote list": cmd/blox_remote_list.md 26 | - "blox repo": cmd/blox_repo.md 27 | - "blox repo init": cmd/blox_repo_init.md 28 | - "blox repo build": cmd/blox_repo_build.md 29 | - "blox schema": cmd/blox_schema.md 30 | - "blox schema new": cmd/blox_schema_new.md 31 | - "blox schema list": cmd/blox_schema_list.md 32 | - "blox schema version": cmd/blox_schema_version.md 33 | - "blox schema version add": cmd/blox_schema_version_add.md 34 | - "Details": 35 | - "Foundations": "foundations.md" 36 | - "Recipes": 37 | - "Common Patterns": "recipes/index.md" 38 | 39 | theme: 40 | features: 41 | - navigation.sections 42 | - navigation.expand 43 | name: material 44 | logo: assets/cueblox-logo.svg 45 | palette: 46 | primary: light blue 47 | accent: pink 48 | -------------------------------------------------------------------------------- /dogfood/sites/docs/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "docs" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["David McKay "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | mkdocs = "^1.1.2" 10 | mkdocs-material = "^7.1.3" 11 | 12 | [tool.poetry.dev-dependencies] 13 | 14 | [build-system] 15 | requires = ["poetry-core>=1.0.0"] 16 | build-backend = "poetry.core.masonry.api" 17 | -------------------------------------------------------------------------------- /dogfood/sites/netlify/api/index.mjs: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import fetch from 'sync-fetch'; 3 | import jsonGraphqlExpress from 'json-graphql-server'; 4 | import serverless from 'serverless-http'; 5 | 6 | const data = fetch( 7 | "https://github.com/cueblox/blox/releases/download/blox/data.json" 8 | ).json(); 9 | 10 | const app = express(); 11 | 12 | const functionName = "serverless-http"; 13 | 14 | app.use("/", jsonGraphqlExpress(data)); 15 | console.log(data); 16 | 17 | exports.handler = serverless(app); 18 | -------------------------------------------------------------------------------- /dogfood/sites/netlify/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Content Repository 9 | 10 | 11 | 12 |
13 |

CueBlox Repository

14 | Live Query 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /dogfood/sites/netlify/netlify.toml: -------------------------------------------------------------------------------- 1 | [functions] 2 | node_bundler = "esbuild" 3 | 4 | [functions.api] 5 | -------------------------------------------------------------------------------- /dogfood/sites/netlify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "0.0.1", 4 | "description": "CueBlox GraphQL API", 5 | "main": "index.mjs", 6 | "scripts": {}, 7 | "author": "David McKay ", 8 | "license": "MIT", 9 | "dependencies": { 10 | "glob": "^7.1.6", 11 | "json-graphql-server": "^2.2.0", 12 | "serverless-http": "2.7.0", 13 | "sync-fetch": "0.3.0" 14 | } 15 | } -------------------------------------------------------------------------------- /dogfood/sites/netlify/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: Arial, Helvetica, sans-serif; 3 | } 4 | 5 | html, 6 | body { 7 | margin: 0; 8 | border: 0; 9 | padding: 0; 10 | background-color: #fff; 11 | } 12 | 13 | main { 14 | margin: auto; 15 | width: 50%; 16 | padding: 20px; 17 | } 18 | 19 | main > h1 { 20 | text-align: center; 21 | font-size: 3.5em; 22 | } 23 | -------------------------------------------------------------------------------- /dogfood/static/images/jpg/main-img-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cueblox/blox/8416baa456e0a05689d152367b4e70266f434c33/dogfood/static/images/jpg/main-img-preview.jpg -------------------------------------------------------------------------------- /dogfood/static/images/png/bketelsens-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cueblox/blox/8416baa456e0a05689d152367b4e70266f434c33/dogfood/static/images/png/bketelsens-report.png -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Floxpkgs/Project Template"; 3 | nixConfig.bash-prompt = "[flox] \\[\\033[38;5;172m\\]λ \\[\\033[0m\\]"; 4 | inputs.floxpkgs.url = "github:flox/floxpkgs"; 5 | 6 | # Declaration of external resources 7 | # ================================= 8 | 9 | # ================================= 10 | 11 | outputs = args @ {floxpkgs, ...}: floxpkgs.project args (_: {}); 12 | } 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cueblox/blox 2 | 3 | go 1.16 4 | 5 | require ( 6 | cuelang.org/go v0.4.2 7 | github.com/cockroachdb/apd/v2 v2.0.2 // indirect 8 | github.com/goccy/go-yaml v1.9.5 9 | github.com/golang/protobuf v1.5.2 // indirect 10 | github.com/google/go-cmp v0.5.7 // indirect 11 | github.com/gookit/color v1.5.0 // indirect 12 | github.com/graphql-go/graphql v0.8.0 13 | github.com/graphql-go/handler v0.2.3 14 | github.com/hashicorp/go-hclog v1.2.0 15 | github.com/hashicorp/go-plugin v1.4.3 16 | github.com/heimdalr/dag v1.0.1 17 | github.com/lib/pq v1.10.4 // indirect 18 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 19 | github.com/otiai10/copy v1.7.0 20 | github.com/pkg/errors v0.9.1 // indirect 21 | github.com/pterm/pterm v0.12.38 22 | github.com/spf13/cobra v1.4.0 23 | github.com/spf13/viper v1.7.0 // indirect 24 | github.com/stretchr/testify v1.7.0 25 | golang.org/x/crypto v0.0.0-20220314234724-5d542ad81a58 // indirect 26 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect 27 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect 28 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 29 | golang.org/x/text v0.3.7 // indirect 30 | google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // indirect 31 | google.golang.org/grpc v1.45.0 // indirect 32 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /internal/cmd/blox.cue: -------------------------------------------------------------------------------- 1 | { 2 | build_dir: "_build" 3 | data_dir: "data" 4 | schemata_dir: "schemata" 5 | static_dir: "static" 6 | } 7 | -------------------------------------------------------------------------------- /internal/cmd/blox_build.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/cueblox/blox/content" 7 | "github.com/pterm/pterm" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type bloxBuildCmd struct { 12 | cmd *cobra.Command 13 | } 14 | 15 | func newBloxBuildCmd() *bloxBuildCmd { 16 | root := &bloxBuildCmd{} 17 | cmd := &cobra.Command{ 18 | Use: "build", 19 | Short: "Validate & Build dataset", 20 | Long: `The build command will ensure that your dataset is correct by 21 | validating it against your schemata. Once validated, it will render all 22 | your content into a single JSON file, which can be consumed by your tooling 23 | of choice. 24 | 25 | Referential Integrity can be enforced with -i. This ensures that any fields 26 | ending with _id are valid references to identifiers within the other content type. 27 | `, 28 | Run: func(cmd *cobra.Command, args []string) { 29 | userConfig, err := ioutil.ReadFile("blox.cue") 30 | 31 | pterm.Debug.Printf("loading user config") 32 | 33 | cobra.CheckErr(err) 34 | 35 | repo, err := content.NewService(string(userConfig), referentialIntegrity) 36 | cobra.CheckErr(err) 37 | 38 | err = repo.RenderAndSave() 39 | cobra.CheckErr(err) 40 | }, 41 | } 42 | cmd.Flags().BoolVarP(&referentialIntegrity, "referential-integrity", "i", false, "Verify referential integrity") 43 | root.cmd = cmd 44 | return root 45 | } 46 | 47 | var referentialIntegrity bool 48 | -------------------------------------------------------------------------------- /internal/cmd/blox_init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | // import go:embed 5 | _ "embed" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "path" 11 | "strings" 12 | 13 | "github.com/pterm/pterm" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | var ( 18 | //go:embed blox.cue 19 | bloxcue string 20 | dataDir string 21 | buildDir string 22 | schemataDir string 23 | staticDir string 24 | starter string 25 | skipConfig bool 26 | ) 27 | 28 | type bloxInitCmd struct { 29 | cmd *cobra.Command 30 | } 31 | 32 | func newBloxInitCmd() *bloxInitCmd { 33 | root := &bloxInitCmd{} 34 | cmd := &cobra.Command{ 35 | Use: "init", 36 | Short: "Create folders and configuration to maintain content with the blox toolset", 37 | Long: `Create a group of folders to store your content. A directory for your data, 38 | schemata, and build output will be created.`, 39 | Run: func(cmd *cobra.Command, args []string) { 40 | if starter != "" { 41 | cobra.CheckErr(installStarter(starter)) 42 | pterm.Info.Println("Starter initialized.") 43 | return 44 | } 45 | // not a starter 46 | err := createDirectories() 47 | cobra.CheckErr(err) 48 | pterm.Info.Println("Initialized folder structures.") 49 | }, 50 | } 51 | cmd.Flags().StringVarP(&dataDir, "data", "d", "data", "where pre-processed content will be stored (source markdown or yaml)") 52 | cmd.Flags().StringVarP(&buildDir, "build", "b", "_build", "where post-processed content will be stored (output json)") 53 | cmd.Flags().StringVarP(&schemataDir, "schemata", "s", "schemata", "where the schemata will be stored") 54 | cmd.Flags().StringVarP(&staticDir, "static", "a", "static", "where the static originals will be found") 55 | cmd.Flags().BoolVarP(&skipConfig, "skip", "c", false, "don't write a configuration file") 56 | 57 | cmd.Flags().StringVarP(&starter, "starter", "t", "", "use a pre-defined starter in the CURRENT directory") 58 | root.cmd = cmd 59 | return root 60 | } 61 | 62 | func createDirectories() error { 63 | pterm.Debug.Printf("Creating directory for data at '%s'\n", dataDir) 64 | err := os.MkdirAll(dataDir, 0o755) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | pterm.Debug.Printf("Creating directory for schemata at '%s'\n", schemataDir) 70 | err = os.MkdirAll(schemataDir, 0o755) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | pterm.Debug.Printf("Creating directory for build output at '%s'\n", buildDir) 76 | err = os.MkdirAll(buildDir, 0o755) 77 | if err != nil { 78 | return err 79 | } 80 | pterm.Debug.Printf("Creating directory for static files at '%s'\n", staticDir) 81 | err = os.MkdirAll(staticDir, 0o755) 82 | if err != nil { 83 | return err 84 | } 85 | pterm.Debug.Println("Creating 'blox.cue' configuration file") 86 | return ioutil.WriteFile("blox.cue", []byte(bloxcue), 0o644) 87 | } 88 | 89 | func installStarter(starter string) error { 90 | pterm.Info.Printf("Installing starter %s\n", starter) 91 | // kinda hacky, look for things that make a url or domain name 92 | // if it's internal, it'll just be one word 93 | internal := !strings.ContainsAny(starter, "/,.") 94 | var repo string 95 | if internal { 96 | repo = fmt.Sprintf("https://github.com/cueblox/starter-%s", starter) 97 | } else { 98 | repo = starter 99 | } 100 | 101 | // git init in the existing directory 102 | cmd := exec.Command("git", "init") 103 | pterm.Info.Println("git init...") 104 | err := cmd.Run() 105 | if err != nil { 106 | pterm.Info.Printf("git init error: %s\n", err) 107 | return err 108 | } 109 | 110 | // add the starter as a remote 111 | cmd = exec.Command("git", "remote", "add", "origin", repo) 112 | pterm.Info.Println("git remote add...") 113 | err = cmd.Run() 114 | if err != nil { 115 | pterm.Info.Printf("git remote error: %s\n", err) 116 | return err 117 | } 118 | // git fetch on the remote 119 | cmd = exec.Command("git", "fetch") 120 | pterm.Info.Println("git fetch...") 121 | err = cmd.Run() 122 | if err != nil { 123 | pterm.Info.Printf("git fetch error: %s\n", err) 124 | return err 125 | } 126 | 127 | // checkout main 128 | cmd = exec.Command("git", "checkout", "origin/main", "-ft") 129 | pterm.Info.Println("git checkout...") 130 | err = cmd.Run() 131 | if err != nil { 132 | pterm.Info.Printf("git checkout error: %s\n", err) 133 | return err 134 | } 135 | here, err := os.Getwd() 136 | if err != nil { 137 | return err 138 | } 139 | 140 | // now remove all traces of the git checkout 141 | err = os.RemoveAll(path.Join(here, ".git")) 142 | 143 | if err != nil { 144 | return err 145 | } 146 | pterm.Info.Println(repo) 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /internal/cmd/blox_new.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "cuelang.org/go/encoding/yaml" 12 | "github.com/cueblox/blox" 13 | "github.com/cueblox/blox/internal/cuedb" 14 | "github.com/cueblox/blox/internal/cueutils" 15 | "github.com/pterm/pterm" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | type bloxNewCmd struct { 20 | cmd *cobra.Command 21 | } 22 | 23 | func newBloxNewCmd() *bloxNewCmd { 24 | root := &bloxNewCmd{} 25 | cmd := &cobra.Command{ 26 | Use: "new", 27 | Short: "Create a new content file for the target dataset", 28 | Long: `This command will allow you to create new content based on the 29 | template attributes within the schemata. By providing a dataset name and ID(slug) 30 | for the new content, you can quickly scaffold new content with ease.`, 31 | Args: cobra.MinimumNArgs(1), 32 | Run: func(cmd *cobra.Command, args []string) { 33 | userConfig, err := ioutil.ReadFile("blox.cue") 34 | cobra.CheckErr(err) 35 | 36 | engine, err := cuedb.NewEngine() 37 | cobra.CheckErr(err) 38 | 39 | cfg, err := blox.NewConfig(blox.BaseConfig) 40 | cobra.CheckErr(err) 41 | 42 | err = cfg.LoadConfigString(string(userConfig)) 43 | cobra.CheckErr(err) 44 | 45 | // Load Schemas! 46 | schemataDir, err := cfg.GetString("schemata_dir") 47 | pterm.Debug.Printf("\t\tSchemata Directory: %s\n", schemataDir) 48 | cobra.CheckErr(err) 49 | 50 | err = filepath.WalkDir(schemataDir, func(path string, d fs.DirEntry, err error) error { 51 | if err != nil { 52 | return err 53 | } 54 | if !d.IsDir() { 55 | bb, err := os.ReadFile(path) 56 | if err != nil { 57 | return err 58 | } 59 | pterm.Debug.Printf("\t\tLoading Schema: %s\n", path) 60 | 61 | err = engine.RegisterSchema(string(bb)) 62 | if err != nil { 63 | return err 64 | } 65 | } 66 | return nil 67 | }) 68 | cobra.CheckErr(err) 69 | 70 | dataSet, err := engine.GetDataSet(dataSetName) 71 | if err != nil { 72 | pterm.Error.Printf("Couldn't find dataset '%s'\n", dataSetName) 73 | pterm.Info.Println("The following DataSets are available:") 74 | dataSets := engine.GetDataSets() 75 | for _, dataSet := range dataSets { 76 | pterm.Info.Printf("\t%s\n", strings.TrimPrefix(dataSet.ID(), "#")) 77 | } 78 | return 79 | } 80 | 81 | templateInstance := engine.CueContext.CompileString("") 82 | cobra.CheckErr(templateInstance.Err()) 83 | 84 | dsp := dataSet.GetDefinitionPath() 85 | dsv := engine.Runtime.Database.LookupPath(dsp) 86 | 87 | templateValue, err := cueutils.CreateFromTemplate(templateInstance.Value(), dsv) 88 | cobra.CheckErr(err) 89 | templateValue = templateValue.LookupPath(dataSet.GetDefinitionPath()) 90 | 91 | dataSetDirectory := fmt.Sprintf("%s/%s", cfg.GetStringOr("data_dir", ""), dataSet.GetDataDirectory()) 92 | 93 | slug := args[0] 94 | pterm.Info.Printf("Creating new %s at %s/%s.yaml\n", dataSet.ID(), dataSetDirectory, slug) 95 | 96 | err = os.MkdirAll(dataSetDirectory, 0o755) 97 | cobra.CheckErr(err) 98 | 99 | bytes, err := yaml.Encode(templateValue) 100 | cobra.CheckErr(err) 101 | 102 | err = ioutil.WriteFile(fmt.Sprintf("%s/%s.yaml", dataSetDirectory, slug), bytes, 0o644) 103 | cobra.CheckErr(err) 104 | }, 105 | } 106 | cmd.Flags().StringVar(&dataSetName, "dataset", "", "Which DataSet to create content for?") 107 | cobra.CheckErr(cmd.MarkFlagRequired("dataset")) 108 | root.cmd = cmd 109 | return root 110 | } 111 | 112 | var dataSetName string 113 | -------------------------------------------------------------------------------- /internal/cmd/blox_render.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | tpl "text/template" 10 | "time" 11 | 12 | "github.com/cueblox/blox/content" 13 | "github.com/pterm/pterm" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | type bloxRenderCmd struct { 18 | cmd *cobra.Command 19 | } 20 | 21 | func newBloxRenderCmd() *bloxRenderCmd { 22 | root := &bloxRenderCmd{} 23 | cmd := &cobra.Command{ 24 | Use: "render", 25 | Short: "Render templates with compiled data", 26 | Long: `Render templates with compiled data. 27 | Use the 'with' parameter to restrict the data set to a single content type. 28 | Use the 'each' parameter to execute the template once for each item.`, 29 | Run: func(cmd *cobra.Command, args []string) { 30 | userConfig, err := ioutil.ReadFile("blox.cue") 31 | 32 | pterm.Debug.Printf("loading user config") 33 | 34 | cobra.CheckErr(err) 35 | 36 | repo, err := content.NewService(string(userConfig), referentialIntegrity) 37 | cobra.CheckErr(err) 38 | 39 | bb, err := repo.RenderJSON() 40 | cobra.CheckErr(err) 41 | 42 | // Load Schemas! 43 | templateDir, err := repo.Cfg.GetString("template_dir") 44 | cobra.CheckErr(err) 45 | pterm.Info.Printf("Using templates from %s\n", templateDir) 46 | if template == "" { 47 | pterm.Error.Println("template name required") 48 | return 49 | } 50 | // Files are provided as a slice of strings. 51 | tplPath := path.Join(templateDir, template) 52 | paths := []string{ 53 | tplPath, 54 | } 55 | _, err = os.Stat(tplPath) 56 | cobra.CheckErr(err) 57 | var dataJson map[string]interface{} 58 | 59 | funcMap := tpl.FuncMap{ 60 | // The name "title" is what the function will be called in the template text. 61 | "rfcdate": func(t string) string { 62 | tm, err := time.Parse("2006-01-02 15:04", t) 63 | if err != nil { 64 | return err.Error() 65 | } 66 | val := tm.Format(time.RFC1123) 67 | return val 68 | }, 69 | "now": func() string { 70 | tm := time.Now() 71 | val := tm.Format(time.RFC1123) 72 | return val 73 | }, 74 | } 75 | 76 | err = json.Unmarshal(bb, &dataJson) 77 | cobra.CheckErr(err) 78 | 79 | t := tpl.Must(tpl.New(template).Funcs(funcMap).ParseFiles(paths...)) 80 | if with != "" { 81 | if each { 82 | dataset, ok := dataJson[with].([]interface{}) 83 | if !ok { 84 | err = errors.New("dataset is not a slice") 85 | cobra.CheckErr(err) 86 | } 87 | for _, thing := range dataset { 88 | err = t.Execute(os.Stdout, thing) 89 | } 90 | } else { 91 | err = t.Execute(os.Stdout, dataJson[with]) 92 | } 93 | } else { 94 | err = t.Execute(os.Stdout, dataJson) 95 | } 96 | cobra.CheckErr(err) 97 | }, 98 | } 99 | cmd.Flags().StringVarP(&template, "template", "t", "", "template to render") 100 | cmd.Flags().StringVarP(&with, "with", "w", "", "dataset to use") 101 | cmd.Flags().BoolVarP(&each, "each", "e", false, "render template once per item") 102 | root.cmd = cmd 103 | return root 104 | } 105 | 106 | var ( 107 | template string 108 | with string 109 | each bool 110 | ) 111 | -------------------------------------------------------------------------------- /internal/cmd/completion.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/spf13/cobra" 4 | 5 | type completionCmd struct { 6 | cmd *cobra.Command 7 | } 8 | 9 | func newCompletionCmd() *completionCmd { 10 | root := &completionCmd{} 11 | cmd := &cobra.Command{ 12 | Use: "completion [bash|zsh|fish]", 13 | Short: "Prints shell autocompletion scripts for blox", 14 | Long: `Allows you to setup your shell to completions blox commands and flags. 15 | 16 | #### Bash 17 | 18 | $ source <(blox completion bash) 19 | 20 | To load completions for each session, execute once: 21 | 22 | ##### Linux 23 | 24 | $ blox completion bash > /etc/bash_completion.d/blox 25 | 26 | ##### MacOS 27 | 28 | $ blox completion bash > /usr/local/etc/bash_completion.d/blox 29 | 30 | #### ZSH 31 | 32 | If shell completion is not already enabled in your environment you will need to enable it. 33 | You can execute the following once: 34 | 35 | $ echo "autoload -U compinit; compinit" >> ~/.zshrc 36 | 37 | To load completions for each session, execute once: 38 | 39 | $ blox completion zsh > "${fpath[1]}/_blox" 40 | 41 | You will need to start a new shell for this setup to take effect. 42 | 43 | #### Fish 44 | 45 | $ blox completion fish | source 46 | 47 | To load completions for each session, execute once: 48 | 49 | $ blox completion fish > ~/.config/fish/completions/blox.fish 50 | 51 | **NOTE**: If you are using an official blox package, it should setup completions for you out of the box. 52 | `, 53 | SilenceUsage: true, 54 | DisableFlagsInUseLine: true, 55 | ValidArgs: []string{"bash", "zsh", "fish"}, 56 | Args: cobra.ExactValidArgs(1), 57 | RunE: func(cmd *cobra.Command, args []string) error { 58 | var err error 59 | switch args[0] { 60 | case "bash": 61 | err = cmd.Root().GenBashCompletion(cmd.OutOrStdout()) 62 | case "zsh": 63 | err = cmd.Root().GenZshCompletion(cmd.OutOrStdout()) 64 | case "fish": 65 | err = cmd.Root().GenFishCompletion(cmd.OutOrStdout(), true) 66 | } 67 | 68 | return err 69 | }, 70 | } 71 | 72 | root.cmd = cmd 73 | return root 74 | } 75 | -------------------------------------------------------------------------------- /internal/cmd/docs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/spf13/cobra/doc" 8 | ) 9 | 10 | type docsCmd struct { 11 | cmd *cobra.Command 12 | } 13 | 14 | func newDocsCmd() *docsCmd { 15 | root := &docsCmd{} 16 | cmd := &cobra.Command{ 17 | Use: "docs", 18 | Short: "Generates blox's command line docs", 19 | SilenceUsage: true, 20 | DisableFlagsInUseLine: true, 21 | Hidden: true, 22 | Args: cobra.NoArgs, 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | root.cmd.Root().DisableAutoGenTag = true 25 | return doc.GenMarkdownTreeCustom(root.cmd.Root(), "dogfood/sites/docs/docs/cmd", func(_ string) string { 26 | return "" 27 | }, func(s string) string { 28 | return "/cmd/" + strings.TrimSuffix(s, ".md") 29 | }) 30 | }, 31 | } 32 | 33 | root.cmd = cmd 34 | return root 35 | } 36 | -------------------------------------------------------------------------------- /internal/cmd/remote.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | type remoteCmd struct { 8 | cmd *cobra.Command 9 | } 10 | 11 | func newRemoteCmd() *remoteCmd { 12 | root := &remoteCmd{} 13 | cmd := &cobra.Command{ 14 | Use: "remote", 15 | Short: "Manage Schemata", 16 | Long: `Blox allows you to consume schemata from remote repositories. 17 | The remote subcommands allow you to list the available schemata from these 18 | repositories, as well as download a schema to your local directories.`, 19 | } 20 | cmd.AddCommand( 21 | newRemoteGetCmd().cmd, 22 | newRemoteListCmd().cmd, 23 | ) 24 | root.cmd = cmd 25 | return root 26 | } 27 | -------------------------------------------------------------------------------- /internal/cmd/remote_get.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | "path" 10 | 11 | "github.com/cueblox/blox" 12 | "github.com/cueblox/blox/internal/repository" 13 | "github.com/pterm/pterm" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | type remoteGetCmd struct { 18 | cmd *cobra.Command 19 | } 20 | 21 | func newRemoteGetCmd() *remoteGetCmd { 22 | root := &remoteGetCmd{} 23 | cmd := &cobra.Command{ 24 | Use: "get ", 25 | Short: "Add a remote schema to your repository", 26 | Args: cobra.ExactArgs(3), 27 | 28 | Run: func(cmd *cobra.Command, args []string) { 29 | repo := args[0] 30 | schemaName := args[1] 31 | version := args[2] 32 | 33 | err := ensureRemote(schemaName, version, repo) 34 | cobra.CheckErr(err) 35 | }, 36 | } 37 | 38 | root.cmd = cmd 39 | return root 40 | } 41 | 42 | // ensureRemote downloads a remote schema at a specific 43 | // version if it doesn't exist locally 44 | // TODO: Duplicated in content package. Consolidate 45 | func ensureRemote(name, version, repo string) error { 46 | // load config 47 | userConfig, err := ioutil.ReadFile("blox.cue") 48 | cobra.CheckErr(err) 49 | 50 | cfg, err := blox.NewConfig(blox.BaseConfig) 51 | cobra.CheckErr(err) 52 | 53 | err = cfg.LoadConfigString(string(userConfig)) 54 | cobra.CheckErr(err) 55 | 56 | // Load Schemas! 57 | schemataDir, err := cfg.GetString("schemata_dir") 58 | cobra.CheckErr(err) 59 | 60 | schemaFilePath := path.Join(schemataDir, fmt.Sprintf("%s_%s.cue", name, version)) 61 | _, err = os.Stat(schemaFilePath) 62 | if os.IsNotExist(err) { 63 | pterm.Info.Printf("Schema does not exist locally: %s_%s.cue\n", name, version) 64 | manifest := fmt.Sprintf("https://%s/manifest.json", repo) 65 | res, err := http.Get(manifest) 66 | cobra.CheckErr(err) 67 | 68 | var repos repository.Repository 69 | err = json.NewDecoder(res.Body).Decode(&repos) 70 | cobra.CheckErr(err) 71 | 72 | var selectedVersion *repository.Version 73 | for _, s := range repos.Schemas { 74 | if s.Name == name { 75 | for _, v := range s.Versions { 76 | if v.Name == version { 77 | selectedVersion = v 78 | pterm.Debug.Println(v.Name, v.Schema, v.Definition) 79 | } 80 | } 81 | } 82 | } 83 | 84 | // make schemata directory 85 | cobra.CheckErr(os.MkdirAll(schemataDir, 0o755)) 86 | 87 | // TODO: don't overwrite each time 88 | filename := fmt.Sprintf("%s_%s.cue", name, version) 89 | filePath := path.Join(schemataDir, filename) 90 | err = os.WriteFile(filePath, []byte(selectedVersion.Definition), 0o755) 91 | cobra.CheckErr(err) 92 | pterm.Info.Printf("Schema downloaded: %s_%s.cue\n", name, version) 93 | return nil 94 | } 95 | pterm.Info.Println("Schema already exists locally, skipping download.") 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /internal/cmd/remote_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/cueblox/blox/internal/repository" 9 | "github.com/pterm/pterm" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | type remoteListCmd struct { 14 | cmd *cobra.Command 15 | } 16 | 17 | func newRemoteListCmd() *remoteListCmd { 18 | root := &remoteListCmd{} 19 | cmd := &cobra.Command{ 20 | Use: "list ", 21 | Short: "List schemata and versions available in a remote repository", 22 | Args: cobra.ExactArgs(1), 23 | 24 | Run: func(cmd *cobra.Command, args []string) { 25 | manifest := fmt.Sprintf("https://%s/manifest.json", args[0]) 26 | res, err := http.Get(manifest) 27 | cobra.CheckErr(err) 28 | 29 | var repos repository.Repository 30 | err = json.NewDecoder(res.Body).Decode(&repos) 31 | cobra.CheckErr(err) 32 | 33 | // TODO extract and reuse with schema_list.go 34 | var td pterm.TableData 35 | header := []string{"Namespace", "Schema", "Version"} 36 | td = append(td, header) 37 | for _, s := range repos.Schemas { 38 | for _, v := range s.Versions { 39 | line := []string{repos.Namespace, s.Name, v.Name} 40 | td = append(td, line) 41 | } 42 | } 43 | 44 | _ = pterm.DefaultTable.WithHasHeader().WithData(td).Render() 45 | }, 46 | } 47 | 48 | root.cmd = cmd 49 | return root 50 | } 51 | -------------------------------------------------------------------------------- /internal/cmd/repo.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func newRepoCmd() *repoCmd { 8 | root := &repoCmd{} 9 | cmd := &cobra.Command{ 10 | Use: "repo", 11 | Short: "Create & Manage Schema Repositories", SilenceUsage: true, 12 | Args: cobra.NoArgs, 13 | } 14 | 15 | root.cmd = cmd 16 | cmd.AddCommand( 17 | newRepoInitCmd().cmd, 18 | newRepoBuildCmd().cmd, 19 | ) 20 | 21 | return root 22 | } 23 | 24 | type repoCmd struct { 25 | cmd *cobra.Command 26 | } 27 | -------------------------------------------------------------------------------- /internal/cmd/repo_build.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/cueblox/blox/internal/repository" 5 | "github.com/pterm/pterm" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type repoBuildCmd struct { 10 | cmd *cobra.Command 11 | } 12 | 13 | func newRepoBuildCmd() *repoBuildCmd { 14 | root := &repoBuildCmd{} 15 | cmd := &cobra.Command{ 16 | Use: "build", 17 | Short: "Build a Schema Repository", 18 | Long: `In order to consume your schema repository with the Blox CLI, you 19 | need to build a manifest file and publish. This command provides the build output 20 | that can be deployed to any static file hosting, or even GitHub raw content links.`, 21 | Args: cobra.NoArgs, 22 | Run: func(cmd *cobra.Command, args []string) { 23 | repo, err := repository.GetRepository() 24 | cobra.CheckErr(err) 25 | pterm.Info.Println("Building Repository") 26 | cobra.CheckErr(repo.Build()) 27 | pterm.Success.Println("Build Complete") 28 | }, 29 | } 30 | root.cmd = cmd 31 | return root 32 | } 33 | -------------------------------------------------------------------------------- /internal/cmd/repo_init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/cueblox/blox/internal/repository" 5 | "github.com/pterm/pterm" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | reporoot string 11 | namespace string 12 | output string 13 | ) 14 | 15 | type repoInitCmd struct { 16 | cmd *cobra.Command 17 | } 18 | 19 | func newRepoInitCmd() *repoInitCmd { 20 | root := &repoInitCmd{} 21 | cmd := &cobra.Command{ 22 | Use: "init", 23 | Short: "Initialize a New Schema Repository", 24 | Long: `Initializing a new schema repository creates the 25 | configuration required to published your schemata.`, 26 | Args: cobra.NoArgs, 27 | Run: func(cmd *cobra.Command, args []string) { 28 | repo, err := repository.NewRepository(namespace, output, reporoot) 29 | cobra.CheckErr(err) 30 | pterm.Success.Printf("Created repository for %s\n", repo.Namespace) 31 | }, 32 | } 33 | 34 | cmd.PersistentFlags().StringVarP(&reporoot, "root", "r", "repository", "directory to store the repository, relative to current directory") 35 | cmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "schemas.you.com", "repository namespace") 36 | cmd.PersistentFlags().StringVarP(&output, "output", "o", "_build", "directory where build output will be written") 37 | root.cmd = cmd 38 | return root 39 | } 40 | -------------------------------------------------------------------------------- /internal/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pterm/pterm" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var ( 11 | quiet bool 12 | debug bool 13 | ) 14 | 15 | func Execute(version string, exit func(int), args []string) { 16 | newRootCmd(version, exit).Execute(args) 17 | } 18 | 19 | type rootCmd struct { 20 | cmd *cobra.Command 21 | exit func(int) 22 | } 23 | 24 | func (cmd *rootCmd) Execute(args []string) { 25 | cmd.cmd.SetArgs(args) 26 | 27 | if err := cmd.cmd.Execute(); err != nil { 28 | fmt.Println(err.Error()) 29 | cmd.exit(1) 30 | } 31 | } 32 | 33 | func newRootCmd(version string, exit func(int)) *rootCmd { 34 | root := &rootCmd{ 35 | exit: exit, 36 | } 37 | cmd := &cobra.Command{ 38 | Use: "blox", 39 | Short: "CueBlox is a suite of slightly opinionated tools for managing and sharing content repositories of YAML and Markdown documents.", 40 | Version: version, 41 | SilenceUsage: true, 42 | SilenceErrors: true, 43 | Args: cobra.NoArgs, 44 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 45 | if debug { 46 | pterm.EnableDebugMessages() 47 | return 48 | } 49 | 50 | if quiet { 51 | pterm.DisableOutput() 52 | } 53 | }, 54 | } 55 | cmd.PersistentFlags().BoolVar(&quiet, "quiet", false, "disable logging") 56 | cmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug logging, overrides 'quiet' flag") 57 | 58 | cmd.AddCommand( 59 | newRemoteCmd().cmd, 60 | newSchemaCmd().cmd, 61 | newRepoCmd().cmd, 62 | newCompletionCmd().cmd, 63 | newDocsCmd().cmd, 64 | newBloxBuildCmd().cmd, 65 | newBloxInitCmd().cmd, 66 | newBloxNewCmd().cmd, 67 | newBloxRenderCmd().cmd, 68 | newBloxServeCmd().cmd, 69 | ) 70 | 71 | root.cmd = cmd 72 | return root 73 | } 74 | -------------------------------------------------------------------------------- /internal/cmd/schema.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | type schemaCmd struct { 8 | cmd *cobra.Command 9 | } 10 | 11 | func newSchemaCmd() *schemaCmd { 12 | root := &schemaCmd{} 13 | cmd := &cobra.Command{ 14 | Use: "schema", 15 | Short: "Create, Manage, and Version your Schemata", 16 | } 17 | 18 | cmd.AddCommand( 19 | newSchemaListCmd().cmd, 20 | newSchemaNewCmd().cmd, 21 | newSchemaVersionCmd().cmd, 22 | ) 23 | 24 | root.cmd = cmd 25 | return root 26 | } 27 | -------------------------------------------------------------------------------- /internal/cmd/schema_list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/cueblox/blox/internal/repository" 5 | "github.com/pterm/pterm" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type schemaListCmd struct { 10 | cmd *cobra.Command 11 | } 12 | 13 | func newSchemaListCmd() *schemaListCmd { 14 | root := &schemaListCmd{} 15 | cmd := &cobra.Command{ 16 | Use: "list", 17 | Short: "List Schemata", 18 | Long: `List schemata that are published via this repository`, 19 | Args: cobra.NoArgs, 20 | Run: func(cmd *cobra.Command, args []string) { 21 | repo, err := repository.GetRepository() 22 | cobra.CheckErr(err) 23 | 24 | var td pterm.TableData 25 | header := []string{"Namespace", "Schema", "Version"} 26 | td = append(td, header) 27 | for _, s := range repo.Schemas { 28 | for _, v := range s.Versions { 29 | line := []string{repo.Namespace, s.Name, v.Name} 30 | td = append(td, line) 31 | } 32 | } 33 | _ = pterm.DefaultTable.WithHasHeader().WithData(td).Render() 34 | }, 35 | } 36 | 37 | root.cmd = cmd 38 | return root 39 | } 40 | -------------------------------------------------------------------------------- /internal/cmd/schema_new.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/cueblox/blox/internal/repository" 5 | "github.com/pterm/pterm" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type schemaNewCmd struct { 10 | cmd *cobra.Command 11 | } 12 | 13 | func newSchemaNewCmd() *schemaNewCmd { 14 | root := &schemaNewCmd{} 15 | cmd := &cobra.Command{ 16 | Use: "new [schema name]", 17 | Short: "Create a New Schema", 18 | Long: `Create a new schema that can be published with the repository management commands`, 19 | Args: cobra.ExactArgs(1), 20 | Run: func(cmd *cobra.Command, args []string) { 21 | repo, err := repository.GetRepository() 22 | cobra.CheckErr(err) 23 | 24 | pterm.Info.Printf("Using repository: %s\n", repo.Namespace) 25 | schema := args[0] 26 | 27 | pterm.Info.Printf("Adding schema: %s\n", schema) 28 | cobra.CheckErr(repo.AddSchema(schema)) 29 | pterm.Success.Printf("Schema %s created\n", schema) 30 | }, 31 | } 32 | 33 | root.cmd = cmd 34 | return root 35 | } 36 | -------------------------------------------------------------------------------- /internal/cmd/schema_validation.cue: -------------------------------------------------------------------------------- 1 | #SchemaMetadata: { 2 | name: string 3 | namespace: string 4 | } 5 | 6 | { 7 | _schema: #SchemaMetadata 8 | } 9 | -------------------------------------------------------------------------------- /internal/cmd/schema_version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | type schemaVersionCmd struct { 8 | cmd *cobra.Command 9 | } 10 | 11 | func newSchemaVersionCmd() *schemaVersionCmd { 12 | root := &schemaVersionCmd{} 13 | cmd := &cobra.Command{ 14 | Use: "version", 15 | Short: "Schema Version Management", 16 | } 17 | cmd.AddCommand( 18 | newSchemaVersionAddCmd().cmd, 19 | ) 20 | root.cmd = cmd 21 | return root 22 | } 23 | -------------------------------------------------------------------------------- /internal/cmd/schema_version_add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/cueblox/blox/internal/repository" 5 | "github.com/pterm/pterm" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type schemaVersionAddCmd struct { 10 | cmd *cobra.Command 11 | } 12 | 13 | func newSchemaVersionAddCmd() *schemaVersionAddCmd { 14 | root := &schemaVersionAddCmd{} 15 | cmd := &cobra.Command{ 16 | Use: "add [schema name]", 17 | Short: "Create a New Version of a Schema", 18 | Args: cobra.ExactArgs(1), 19 | Run: func(cmd *cobra.Command, args []string) { 20 | repo, err := repository.GetRepository() 21 | cobra.CheckErr(err) 22 | 23 | pterm.Info.Printf("Using repository: %s\n", repo.Namespace) 24 | schema := args[0] 25 | 26 | pterm.Info.Printf("Creating schema: %s\n", schema) 27 | cobra.CheckErr(repo.AddVersion(schema)) 28 | pterm.Success.Printf("Schema %s created\n", schema) 29 | }, 30 | } 31 | 32 | root.cmd = cmd 33 | return root 34 | } 35 | -------------------------------------------------------------------------------- /internal/cmd/serve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "path/filepath" 7 | 8 | "github.com/cueblox/blox/content" 9 | "github.com/pterm/pterm" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | type bloxServeCmd struct { 14 | cmd *cobra.Command 15 | } 16 | 17 | func newBloxServeCmd() *bloxServeCmd { 18 | root := &bloxServeCmd{} 19 | cmd := &cobra.Command{ 20 | Use: "serve", 21 | Short: "Serve a GraphQL API", 22 | 23 | Run: func(cmd *cobra.Command, args []string) { 24 | userConfig, err := ioutil.ReadFile("blox.cue") 25 | 26 | pterm.Debug.Printf("loading user config") 27 | 28 | cobra.CheckErr(err) 29 | 30 | repo, err := content.NewService(string(userConfig), referentialIntegrity) 31 | cobra.CheckErr(err) 32 | 33 | if static { 34 | staticDir, err := repo.Cfg.GetString("static_dir") 35 | pterm.Info.Printf("Serving static files from %s\n", staticDir) 36 | 37 | cobra.CheckErr(err) 38 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(filepath.Join(".", staticDir))))) 39 | } 40 | 41 | hf, err := repo.GQLHandlerFunc() 42 | cobra.CheckErr(err) 43 | http.HandleFunc("/", hf) 44 | 45 | h, err := repo.GQLPlaygroundHandler() 46 | cobra.CheckErr(err) 47 | http.Handle("/ui", h) 48 | 49 | pterm.Info.Printf("Server is running at %s\n", address) 50 | cobra.CheckErr(http.ListenAndServe(address, nil)) 51 | }, 52 | } 53 | cmd.Flags().BoolVarP(&static, "static", "s", true, "Serve static files") 54 | cmd.Flags().StringVarP(&address, "address", "a", ":8080", "Listen address") 55 | 56 | root.cmd = cmd 57 | return root 58 | } 59 | 60 | var ( 61 | static bool 62 | address string 63 | ) 64 | -------------------------------------------------------------------------------- /internal/cuedb/config.cue: -------------------------------------------------------------------------------- 1 | { 2 | build_dir: string | *"_build" 3 | data_dir: string | *"data" 4 | schemata_dir: string | *"schemata" 5 | static_dir: string | *"static" 6 | } 7 | -------------------------------------------------------------------------------- /internal/cuedb/dataset.go: -------------------------------------------------------------------------------- 1 | package cuedb 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "cuelang.org/go/cue" 8 | ) 9 | 10 | // Can't use schemaField, yet. 11 | const SchemaMetadataCue = `{ 12 | _schema: { 13 | namespace: string 14 | name: string 15 | } 16 | }` 17 | 18 | type SchemaMetadata struct { 19 | Namespace string 20 | Name string 21 | } 22 | 23 | // Can't use dataSetField, yet. 24 | const DataSetMetadataCue = `{ 25 | _dataset: { 26 | plural: string 27 | supportedExtensions: [...string] 28 | } 29 | }` 30 | 31 | type DataSetMetadata struct { 32 | Plural string 33 | SupportedExtensions []string 34 | } 35 | 36 | type DataSet struct { 37 | name string 38 | schemaMetadata SchemaMetadata 39 | schema cue.Value 40 | cuePath cue.Path 41 | metadata DataSetMetadata 42 | relationships []string 43 | } 44 | 45 | func (d *DataSet) GetDataMapCue() string { 46 | return fmt.Sprintf(`{ 47 | %s: %s: _ 48 | %s: %s: [ID=string]: %s.%s & {id: (ID)} 49 | }`, 50 | d.GetInlinePath(), d.name, 51 | dataPathRoot, d.metadata.Plural, d.cuePath.String(), d.name, 52 | ) 53 | } 54 | 55 | func (d *DataSet) GetPluralName() string { 56 | return strings.Title(d.metadata.Plural) 57 | } 58 | 59 | func (d *DataSet) GetExternalName() string { 60 | return strings.Replace(d.name, "#", "", 1) 61 | } 62 | 63 | func (d *DataSet) GetSchemaCue() cue.Value { 64 | return d.schema 65 | } 66 | 67 | func (d *DataSet) CueDataPath() cue.Path { 68 | return cue.ParsePath(fmt.Sprintf("%s.%s", dataPathRoot, d.metadata.Plural)) 69 | } 70 | 71 | func (d *DataSet) IsSupportedExtension(ext string) bool { 72 | for _, val := range d.metadata.SupportedExtensions { 73 | if val == ext { 74 | return true 75 | } 76 | } 77 | 78 | return false 79 | } 80 | 81 | func (d *DataSet) GetSupportedExtensions() []string { 82 | return d.metadata.SupportedExtensions 83 | } 84 | -------------------------------------------------------------------------------- /internal/cuedb/graphql.go: -------------------------------------------------------------------------------- 1 | package cuedb 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "cuelang.org/go/cue" 8 | "github.com/graphql-go/graphql" 9 | ) 10 | 11 | type GraphQlObjectGlue struct { 12 | Object *graphql.Object 13 | Engine *Engine 14 | Resolver func(p graphql.ResolveParams) (interface{}, error) 15 | } 16 | 17 | func CueValueToGraphQlField(existingObjects map[string]GraphQlObjectGlue, dataSet DataSet, cueValue cue.Value) (graphql.Fields, error) { 18 | fields, err := cueValue.Fields(cue.All()) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | graphQlFields := make(map[string]*graphql.Field) 24 | 25 | for fields.Next() { 26 | if strings.HasPrefix(fields.Label(), "_") { 27 | continue 28 | } 29 | 30 | if fields.Selector().IsDefinition() { 31 | continue 32 | } 33 | 34 | relationshipLabel := fields.Label() 35 | relationship := fields.Value().Attribute("relationship") 36 | 37 | switch fields.Value().IncompleteKind() { 38 | case cue.StructKind: 39 | subFields, err := CueValueToGraphQlField(existingObjects, dataSet, fields.Value()) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | graphQlFields[fields.Label()] = &graphql.Field{ 45 | Type: graphql.NewObject(graphql.ObjectConfig{ 46 | Name: fmt.Sprintf("%s%s", dataSet.GetExternalName(), strings.Title(fields.Label())), 47 | Fields: subFields, 48 | }), 49 | } 50 | 51 | case cue.ListKind: 52 | listOf := fields.Value().LookupPath(cue.MakePath(cue.AnyIndex)) 53 | 54 | kind, err := CueValueToGraphQlType(listOf) 55 | if err == nil { 56 | if err = relationship.Err(); err == nil { 57 | graphQlFields[fields.Label()] = &graphql.Field{ 58 | Type: graphql.NewList(existingObjects[relationship.Contents()].Object), 59 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 60 | data := existingObjects[relationship.Contents()].Engine.GetAllData(fmt.Sprintf("#%s", relationship.Contents())) 61 | 62 | records := make(map[string]interface{}) 63 | if err = data.Decode(&records); err != nil { 64 | return nil, err 65 | } 66 | 67 | source, ok := p.Source.(map[string]interface{}) 68 | 69 | if !ok { 70 | return nil, nil 71 | } 72 | 73 | searchIds := source[relationshipLabel].([]interface{}) 74 | returnRecords := []interface{}{} 75 | 76 | for recordID, record := range records { 77 | for _, searchID := range searchIds { 78 | if string(recordID) == searchID.(string) { 79 | returnRecords = append(returnRecords, record) 80 | } 81 | } 82 | } 83 | 84 | return returnRecords, nil 85 | }, 86 | } 87 | } else { 88 | graphQlFields[fields.Label()] = &graphql.Field{ 89 | Type: &graphql.List{ 90 | OfType: kind, 91 | }, 92 | } 93 | } 94 | continue 95 | } 96 | 97 | // List of non-scalar types 98 | subFields, err := CueValueToGraphQlField(existingObjects, dataSet, listOf.Value()) 99 | 100 | // No error, so we know this is a simple value or struct 101 | if err == nil { 102 | graphQlFields[fields.Label()] = &graphql.Field{ 103 | Type: &graphql.List{OfType: graphql.NewObject(graphql.ObjectConfig{ 104 | Name: fmt.Sprintf("%s%s", dataSet.GetExternalName(), strings.Title(fields.Label())), 105 | Fields: subFields, 106 | })}, 107 | } 108 | continue 109 | } 110 | 111 | // Error, probably a disjunction or other complex value 112 | // Not handled, yet. 113 | // switch listOf.IncompleteKind() { 114 | // case cue.StructKind: 115 | // fields, err := listOf.Fields() 116 | // fmt.Println(err) 117 | // fmt.Println(fields) 118 | // } 119 | 120 | return nil, err 121 | 122 | case cue.BoolKind, cue.FloatKind, cue.IntKind, cue.NumberKind, cue.StringKind: 123 | kind, err := CueValueToGraphQlType(fields.Value()) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | if err = relationship.Err(); err == nil { 129 | graphQlFields[fields.Label()] = &graphql.Field{ 130 | Type: existingObjects[relationship.Contents()].Object, 131 | Resolve: func(p graphql.ResolveParams) (interface{}, error) { 132 | data := existingObjects[relationship.Contents()].Engine.GetAllData(fmt.Sprintf("#%s", relationship.Contents())) 133 | 134 | records := make(map[string]interface{}) 135 | if err = data.Decode(&records); err != nil { 136 | return nil, err 137 | } 138 | 139 | source, ok := p.Source.(map[string]interface{}) 140 | 141 | if !ok { 142 | return nil, nil 143 | } 144 | 145 | for recordID, record := range records { 146 | if string(recordID) == source[relationshipLabel].(string) { 147 | return record, nil 148 | } 149 | } 150 | 151 | return nil, nil 152 | }, 153 | } 154 | } else if fields.IsOptional() { 155 | graphQlFields[fields.Label()] = &graphql.Field{ 156 | Type: kind, 157 | } 158 | } else { 159 | graphQlFields[fields.Label()] = &graphql.Field{ 160 | Type: &graphql.NonNull{ 161 | OfType: kind, 162 | }, 163 | } 164 | } 165 | } 166 | } 167 | 168 | return graphQlFields, nil 169 | } 170 | 171 | func CueValueToGraphQlType(value cue.Value) (*graphql.Scalar, error) { 172 | switch value.IncompleteKind() { 173 | case cue.BoolKind: 174 | return graphql.Boolean, nil 175 | case cue.FloatKind: 176 | return graphql.Float, nil 177 | case cue.IntKind: 178 | return graphql.Int, nil 179 | case cue.NumberKind: 180 | return graphql.Float, nil 181 | case cue.StringKind: 182 | return graphql.String, nil 183 | } 184 | 185 | return nil, fmt.Errorf("unhandled type: %v", value.IncompleteKind()) 186 | } 187 | -------------------------------------------------------------------------------- /internal/cuedb/graphql_test.go: -------------------------------------------------------------------------------- 1 | package cuedb 2 | 3 | import ( 4 | "testing" 5 | 6 | "cuelang.org/go/cue/cuecontext" 7 | "github.com/graphql-go/graphql" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGraphqlGeneration(t *testing.T) { 12 | type test struct { 13 | cueLiteral string 14 | dataSet *DataSet 15 | expected graphql.Fields 16 | } 17 | 18 | tests := []test{ 19 | { 20 | dataSet: &DataSet{name: "Type"}, 21 | cueLiteral: "t1: string", expected: graphql.Fields{ 22 | "t1": {Type: &graphql.NonNull{OfType: graphql.String}}, 23 | }, 24 | }, 25 | { 26 | dataSet: &DataSet{name: "Type"}, 27 | cueLiteral: "t2: int", expected: graphql.Fields{ 28 | "t2": {Type: &graphql.NonNull{OfType: graphql.Int}}, 29 | }, 30 | }, 31 | { 32 | dataSet: &DataSet{name: "Type"}, 33 | cueLiteral: "t3: [...int]", expected: graphql.Fields{ 34 | "t3": {Type: &graphql.List{OfType: graphql.Int}}, 35 | }, 36 | }, 37 | { 38 | dataSet: &DataSet{name: "Type"}, 39 | cueLiteral: "t4: [...string]", expected: graphql.Fields{ 40 | "t4": {Type: &graphql.List{OfType: graphql.String}}, 41 | }, 42 | }, 43 | { 44 | dataSet: &DataSet{name: "Type"}, 45 | cueLiteral: "t5: string, t6?: int", expected: graphql.Fields{ 46 | "t5": {Type: &graphql.NonNull{OfType: graphql.String}}, 47 | "t6": {Type: graphql.Int}, 48 | }, 49 | }, 50 | { 51 | dataSet: &DataSet{name: "Type"}, 52 | cueLiteral: "{ t7: string, t8: { t9: string, t10: string} }", expected: graphql.Fields{ 53 | "t7": {Type: &graphql.NonNull{OfType: graphql.String}}, 54 | "t8": {Type: graphql.NewObject(graphql.ObjectConfig{ 55 | Name: "TypeT8", 56 | Fields: graphql.Fields{ 57 | "t9": {Type: &graphql.NonNull{OfType: graphql.String}}, 58 | "t10": {Type: &graphql.NonNull{OfType: graphql.String}}, 59 | }, 60 | })}, 61 | }, 62 | }, 63 | { 64 | dataSet: &DataSet{name: "Type"}, 65 | cueLiteral: "{ t11: string, t12: { t13: string, t14: string, t15: { t16: int } } }", expected: graphql.Fields{ 66 | "t11": {Type: &graphql.NonNull{OfType: graphql.String}}, 67 | "t12": {Type: graphql.NewObject(graphql.ObjectConfig{ 68 | Name: "TypeT12", 69 | Fields: graphql.Fields{ 70 | "t13": {Type: &graphql.NonNull{OfType: graphql.String}}, 71 | "t14": {Type: &graphql.NonNull{OfType: graphql.String}}, 72 | "t15": {Type: graphql.NewObject(graphql.ObjectConfig{ 73 | Name: "TypeT15", 74 | Fields: graphql.Fields{ 75 | "t16": {Type: &graphql.NonNull{OfType: graphql.Int}}, 76 | }, 77 | })}, 78 | }, 79 | })}, 80 | }, 81 | }, 82 | { 83 | dataSet: &DataSet{name: "Type"}, 84 | cueLiteral: "{ #Test: { t19: string}\nt17: string, t18: #Test }", expected: graphql.Fields{ 85 | "t17": {Type: &graphql.NonNull{OfType: graphql.String}}, 86 | "t18": {Type: graphql.NewObject(graphql.ObjectConfig{ 87 | Name: "TypeT18", 88 | Fields: graphql.Fields{ 89 | "t19": {Type: &graphql.NonNull{OfType: graphql.String}}, 90 | }, 91 | })}, 92 | }, 93 | }, 94 | { 95 | dataSet: &DataSet{name: "Type"}, 96 | cueLiteral: "{ #Test: { t20: string}\n t21: string, t22: [ ... #Test ] }", expected: graphql.Fields{ 97 | "t21": {Type: &graphql.NonNull{OfType: graphql.String}}, 98 | "t22": {Type: &graphql.List{OfType: graphql.NewObject(graphql.ObjectConfig{ 99 | Name: "TypeT22", 100 | Fields: graphql.Fields{ 101 | "t20": {Type: &graphql.NonNull{OfType: graphql.String}}, 102 | }, 103 | })}}, 104 | }, 105 | }, 106 | // WIP 107 | // Aim is to "flatten" disjunctions into a single struct 108 | // {cueLiteral: "{ #A: {t23: string}\n#B: { t24?: string}\n t25: string, t26: [ ... #A | #B ] }", expected: graphql.Fields{ 109 | // "t25": {Type: &graphql.NonNull{OfType: graphql.String}}, 110 | // "t26": {Type: &graphql.List{OfType: graphql.NewObject(graphql.ObjectConfig{ 111 | // Fields: graphql.Fields{ 112 | // "t23": {Type: &graphql.NonNull{OfType: graphql.String}}, 113 | // "t24": {Type: graphql.String}, 114 | // }, 115 | // })}}, 116 | // }}, 117 | } 118 | 119 | cueContext := cuecontext.New() 120 | 121 | for _, tc := range tests { 122 | cueValue := cueContext.CompileString(tc.cueLiteral) 123 | assert.Equal(t, nil, cueValue.Err()) 124 | 125 | graphqlObjects := make(map[string]GraphQlObjectGlue) 126 | 127 | graphQlObject, err := CueValueToGraphQlField(graphqlObjects, *tc.dataSet, cueValue) 128 | assert.Equal(t, nil, err) 129 | assert.EqualValues(t, tc.expected, graphQlObject) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /internal/cueutils/utils.go: -------------------------------------------------------------------------------- 1 | package cueutils 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | 7 | "cuelang.org/go/cue" 8 | "cuelang.org/go/cue/ast" 9 | "github.com/pterm/pterm" 10 | ) 11 | 12 | // GetAcceptedValues returns the values constraints 13 | // for a cue node 14 | func GetAcceptedValues(node ast.Node) ([]string, error) { 15 | switch v := node.(type) { 16 | case *ast.Ident: 17 | return []string{v.Name}, nil 18 | 19 | case *ast.ListLit: 20 | return []string{"list"}, nil 21 | } 22 | 23 | return []string{"None"}, nil 24 | } 25 | 26 | func CreateFromTemplate(valueOut cue.Value, valueIn cue.Value) (cue.Value, error) { 27 | fieldIterator, err := valueIn.Fields(cue.Optional(true)) 28 | if err != nil { 29 | return valueOut, err 30 | } 31 | 32 | for fieldIterator.Next() { 33 | fieldValue := fieldIterator.Value() 34 | 35 | if cue.StructKind == fieldValue.IncompleteKind() { 36 | valueOut, err = CreateFromTemplate(valueOut, fieldValue) 37 | if err != nil { 38 | return valueOut, err 39 | } 40 | continue 41 | } 42 | 43 | templateAttribute := fieldValue.Attribute("template") 44 | if err = templateAttribute.Err(); err != nil { 45 | // For now, we just skip 46 | continue 47 | } 48 | 49 | templateValue := strings.TrimPrefix(templateAttribute.Contents(), `"`) 50 | templateValue = strings.TrimSuffix(templateValue, `"`) 51 | 52 | switch fieldValue.IncompleteKind() { 53 | case cue.StringKind: 54 | valueOut = valueOut.FillPath(fieldValue.Path(), templateValue) 55 | 56 | case cue.IntKind: 57 | i, err := strconv.Atoi(templateValue) 58 | if err != nil { 59 | return valueOut, err 60 | } 61 | valueOut = valueOut.FillPath(fieldValue.Path(), i) 62 | 63 | case cue.BoolKind: 64 | b, err := strconv.ParseBool(templateValue) 65 | if err != nil { 66 | return valueOut, err 67 | } 68 | valueOut = valueOut.FillPath(fieldValue.Path(), b) 69 | 70 | case cue.ListKind: 71 | listValue := strings.Split(templateValue, ",") 72 | valueOut = valueOut.FillPath(fieldValue.Path(), listValue) 73 | 74 | default: 75 | // Default, just assume string and drop in the value 76 | pterm.Debug.Println("UNMATCHED", fieldValue.IncompleteKind()) 77 | valueOut = valueOut.FillPath(fieldValue.Path(), templateValue) 78 | } 79 | } 80 | 81 | return valueOut, nil 82 | } 83 | -------------------------------------------------------------------------------- /internal/cueutils/utils_test.go: -------------------------------------------------------------------------------- 1 | package cueutils 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "cuelang.org/go/cue" 8 | "cuelang.org/go/cue/ast" 9 | "cuelang.org/go/cue/cuecontext" 10 | "cuelang.org/go/cue/parser" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func getAstNodeValue(cue string, label string) (ast.Node, error) { 15 | lAst, err := parser.ParseFile("", cue) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | for _, decl := range lAst.Decls { 21 | if field, ok := decl.(*ast.Field); ok { 22 | if fieldName, ok := field.Label.(*ast.Ident); ok { 23 | if label == fieldName.Name { 24 | return field.Value, nil 25 | } 26 | } 27 | } 28 | } 29 | 30 | return nil, errors.New("Couldn't find field with label") 31 | } 32 | 33 | func TestGetAcceptedValuesString(t *testing.T) { 34 | node, err := getAstNodeValue("field_name: string", "field_name") 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | acceptedValues, err := GetAcceptedValues(node) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | assert.ElementsMatch(t, []string{"string"}, acceptedValues) 45 | } 46 | 47 | func TestGetAcceptedValuesInt(t *testing.T) { 48 | node, err := getAstNodeValue("field_name: int", "field_name") 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | acceptedValues, err := GetAcceptedValues(node) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | assert.ElementsMatch(t, []string{"int"}, acceptedValues) 59 | } 60 | 61 | func TestGetAcceptedValuesFloat(t *testing.T) { 62 | node, err := getAstNodeValue("field_name: float", "field_name") 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | acceptedValues, err := GetAcceptedValues(node) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | assert.ElementsMatch(t, []string{"float"}, acceptedValues) 73 | } 74 | 75 | func TestGetAcceptedValuesNumber(t *testing.T) { 76 | node, err := getAstNodeValue("field_name: number", "field_name") 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | 81 | acceptedValues, err := GetAcceptedValues(node) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | assert.ElementsMatch(t, []string{"number"}, acceptedValues) 87 | } 88 | 89 | func TestGetAcceptedValuesBool(t *testing.T) { 90 | node, err := getAstNodeValue("field_name: bool", "field_name") 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | 95 | acceptedValues, err := GetAcceptedValues(node) 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | 100 | assert.ElementsMatch(t, []string{"bool"}, acceptedValues) 101 | } 102 | 103 | func TestGetAcceptedValuesList(t *testing.T) { 104 | node, err := getAstNodeValue("field_name: [string]", "field_name") 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | 109 | acceptedValues, err := GetAcceptedValues(node) 110 | if err != nil { 111 | t.Fatal(err) 112 | } 113 | 114 | assert.ElementsMatch(t, []string{"list"}, acceptedValues) 115 | } 116 | 117 | func TestCreateFromTemplate(t *testing.T) { 118 | cueWithTemplateAttributes := `{ 119 | // NameComment 120 | name: string @template(Random Name) //NameInlineComments 121 | age: int @template(21) 122 | happy: bool @template(true) 123 | scottish: bool @template(false) 124 | movies: [...string] @template(The Matrix, Top Gun) 125 | social: { 126 | network: string @template(twitter) 127 | name: string @template(rawkode) 128 | } 129 | }` 130 | 131 | cueContext := cuecontext.New() 132 | cueValue := cueContext.CompileString(cueWithTemplateAttributes) 133 | assert.Equal(t, nil, cueValue.Err()) 134 | 135 | cueTemplate, err := CreateFromTemplate(cueValue, cueValue) 136 | assert.Equal(t, nil, err) 137 | 138 | name, err := cueTemplate.LookupPath(cue.ParsePath("name")).String() 139 | assert.Equal(t, nil, err) 140 | assert.Equal(t, "Random Name", name) 141 | 142 | age, err := cueTemplate.LookupPath(cue.ParsePath("age")).Int64() 143 | assert.Equal(t, nil, err) 144 | assert.Equal(t, int64(21), age) 145 | 146 | happy, err := cueTemplate.LookupPath(cue.ParsePath("happy")).Bool() 147 | assert.Equal(t, nil, err) 148 | assert.Equal(t, true, happy) 149 | 150 | scottish, err := cueTemplate.LookupPath(cue.ParsePath("scottish")).Bool() 151 | assert.Equal(t, nil, err) 152 | assert.Equal(t, false, scottish) 153 | 154 | social := cueTemplate.LookupPath(cue.ParsePath("social")) 155 | 156 | socialNetwork, err := social.LookupPath(cue.ParsePath("network")).String() 157 | assert.Equal(t, nil, err) 158 | assert.Equal(t, "twitter", socialNetwork) 159 | 160 | socialName, err := social.LookupPath(cue.ParsePath("name")).String() 161 | assert.Equal(t, nil, err) 162 | assert.Equal(t, "rawkode", socialName) 163 | 164 | movies, err := cueTemplate.LookupPath(cue.ParsePath("movies")).List() 165 | expectedMovies := []string{"The Matrix"} 166 | assert.Equal(t, nil, err) 167 | for _, expectedMovie := range expectedMovies { 168 | movies.Next() 169 | movieString, err := movies.Value().String() 170 | assert.Equal(t, nil, err) 171 | assert.Equal(t, expectedMovie, movieString) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /internal/encoding/markdown/markdown.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // ToYAML converts markdown to a YAML file 8 | // storing the implicit 'body' of the markdown 9 | // in 'body' 10 | func ToYAML(raw string) (string, error) { 11 | var content strings.Builder 12 | var err error 13 | lines := strings.Split(raw, "\n") 14 | var inBody bool 15 | for i, line := range lines { 16 | // remove first delimiter 17 | if i != 0 { 18 | if !inBody { 19 | // this is last delimiter 20 | // replace with 'body: |' and 21 | // indent the rest of the body by 2 spaces 22 | if line == "---" { 23 | content.WriteString("body: |") 24 | content.WriteString("\n") 25 | inBody = true 26 | } else { 27 | content.WriteString(line) 28 | content.WriteString("\n") 29 | } 30 | } else { 31 | content.WriteString(" ") 32 | content.WriteString(line) 33 | content.WriteString("\n") 34 | } 35 | } 36 | } 37 | return content.String(), err 38 | } 39 | -------------------------------------------------------------------------------- /internal/encoding/markdown/markdown_test.go: -------------------------------------------------------------------------------- 1 | package markdown 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestFormat(t *testing.T) { 9 | in := `--- 10 | key1: value 11 | key2: value2 12 | --- 13 | --- 14 | revealKey: value 15 | --- 16 | My Body 17 | --- 18 | Body Line 2` 19 | expected := `key1: value 20 | key2: value2 21 | body: | 22 | --- 23 | revealKey: value 24 | --- 25 | My Body 26 | --- 27 | Body Line 2 28 | ` 29 | output, err := ToYAML(in) 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | if output != expected { 34 | fmt.Println(expected) 35 | fmt.Println(output) 36 | t.Error("output doesn't match expected") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/hosting/helpers.go: -------------------------------------------------------------------------------- 1 | package hosting 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // CreateFileWithContents is a helper for hosting 9 | func CreateFileWithContents(path string, contents string) error { 10 | f, err := os.Create(path) 11 | if err != nil { 12 | return fmt.Errorf("creating file: %s", err) 13 | } 14 | defer f.Close() 15 | 16 | _, err = f.WriteString(contents) 17 | return err 18 | } 19 | -------------------------------------------------------------------------------- /internal/repository/blox.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | // import go:embed 5 | _ "embed" 6 | ) 7 | 8 | // Config stores information about 9 | // the repository location 10 | type Config struct { 11 | RepositoryRoot string `json:"repository_root"` 12 | Namespace string `json:"namespace"` 13 | OutputDirectory string `json:"output_dir"` 14 | } 15 | 16 | //go:embed schema.cue 17 | var schemaCue []byte 18 | -------------------------------------------------------------------------------- /internal/repository/config.cue: -------------------------------------------------------------------------------- 1 | { 2 | repository_root: string | *"repository" 3 | namespace: string 4 | build_dir: string | *"_build" 5 | } 6 | -------------------------------------------------------------------------------- /internal/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "encoding/json" 7 | "fmt" 8 | "io/fs" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/cueblox/blox" 15 | "github.com/otiai10/copy" 16 | "github.com/pterm/pterm" 17 | ) 18 | 19 | //go:embed config.cue 20 | var BaseRepositoryConfig string 21 | 22 | // Repository is a group of schemas 23 | type Repository struct { 24 | Root string 25 | Namespace string 26 | Output string 27 | Schemas []*Schema 28 | } 29 | 30 | // GetRepository returns the Repository 31 | // described by the repository.cue file in the 32 | // current directory 33 | func GetRepository() (*Repository, error) { 34 | // initialize config engine with defaults 35 | cfg, err := blox.NewConfig(BaseRepositoryConfig) 36 | if err != nil { 37 | return nil, err 38 | } 39 | // load user config 40 | err = cfg.LoadConfig("repository.cue") 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | build_dir, err := cfg.GetString("output_dir") 46 | pterm.Debug.Printf("\t\tBuild Directory: %s\n", build_dir) 47 | if err != nil { 48 | return nil, err 49 | } 50 | namespace, err := cfg.GetString("namespace") 51 | 52 | pterm.Debug.Printf("\t\tNamespace: %s\n", namespace) 53 | if err != nil { 54 | return nil, err 55 | } 56 | reporoot, err := cfg.GetString("repository_root") 57 | pterm.Debug.Printf("\t\tRepository Root: %s\n", reporoot) 58 | if err != nil { 59 | return nil, err 60 | } 61 | r := &Repository{ 62 | Namespace: namespace, 63 | Root: reporoot, 64 | Output: build_dir, 65 | } 66 | err = r.load() 67 | if err != nil { 68 | return nil, err 69 | } 70 | return r, nil 71 | } 72 | 73 | // NewRepository creates a new repository root and writes 74 | // the metadata information 75 | func NewRepository(namespace, output, root string) (*Repository, error) { 76 | r := &Repository{ 77 | Root: root, 78 | Namespace: namespace, 79 | Output: output, 80 | } 81 | // create the repository directory 82 | pterm.Debug.Printf("\t\tCreating repository root directory at %s\n", root) 83 | err := r.createRoot() 84 | if err != nil { 85 | return nil, err 86 | } 87 | // write the config file 88 | 89 | err = r.writeConfig() 90 | if err != nil { 91 | return nil, err 92 | } 93 | return r, nil 94 | } 95 | 96 | func (r *Repository) load() error { 97 | // load schemas and versions recursively 98 | r.Schemas = make([]*Schema, 0) 99 | schemaPath := r.Root 100 | err := filepath.WalkDir(schemaPath, func(path string, d fs.DirEntry, err error) error { 101 | if err != nil { 102 | pterm.Error.Printf("failure accessing a path %q: %v\n", path, err) 103 | return err 104 | } 105 | // be friendly to our Windows neighbors :) 106 | paths := strings.Split(path, string(os.PathSeparator)) 107 | if d.IsDir() { 108 | if d.Name() == r.Root { 109 | return nil 110 | } 111 | if d.Name() == r.Output { 112 | return nil 113 | } 114 | if len(paths) == 2 { 115 | // this is a schema 116 | // process 117 | s := &Schema{ 118 | Namespace: r.Namespace, 119 | Name: d.Name(), 120 | } 121 | r.Schemas = append(r.Schemas, s) 122 | return nil 123 | } 124 | 125 | if len(paths) == 3 { 126 | // this is a version 127 | 128 | // process 129 | v := &Version{ 130 | Namespace: r.Namespace, 131 | Name: d.Name(), 132 | } 133 | for _, s := range r.Schemas { 134 | if s.Name == paths[1] { 135 | v.Schema = paths[1] 136 | s.Versions = append(s.Versions, v) 137 | } 138 | } 139 | return nil 140 | } 141 | 142 | } else { 143 | // not a dir, must be file 144 | // we only care about files in 145 | // version directories 146 | if len(paths) == 4 { 147 | if d.Name() == "schema.cue" { 148 | bb, err := os.ReadFile(path) 149 | if err != nil { 150 | return err 151 | } 152 | for _, s := range r.Schemas { 153 | if s.Name == paths[1] { 154 | for _, v := range s.Versions { 155 | if v.Name == paths[2] { 156 | buf := bytes.NewBuffer([]byte{}) 157 | json.HTMLEscape(buf, bb) 158 | v.Definition = buf.String() 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | 167 | return nil 168 | }) 169 | 170 | return err 171 | } 172 | 173 | func (r *Repository) writeConfig() error { 174 | b := &Config{ 175 | RepositoryRoot: r.Root, 176 | Namespace: r.Namespace, 177 | OutputDirectory: r.Output, 178 | } 179 | bb, err := json.Marshal(b) 180 | if err != nil { 181 | return err 182 | } 183 | wd, err := os.Getwd() 184 | if err != nil { 185 | return err 186 | } 187 | configPath := path.Join(wd, "repository.cue") 188 | return os.WriteFile(configPath, bb, 0o755) 189 | } 190 | 191 | func (r *Repository) createRoot() error { 192 | wd, err := os.Getwd() 193 | if err != nil { 194 | return err 195 | } 196 | repoPath := path.Join(wd, r.Root) 197 | err = os.MkdirAll(repoPath, 0o755) 198 | return err 199 | } 200 | 201 | // AddSchema creates a new directory for a schema 202 | // and creates the first version of the schema. 203 | func (r *Repository) AddSchema(name string) error { 204 | // create the schema directory 205 | schemaPath := path.Join(r.Root, name) 206 | err := os.MkdirAll(schemaPath, 0o744) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | // create the first version 212 | versionPath := path.Join(schemaPath, "v1") 213 | err = os.MkdirAll(versionPath, 0o744) 214 | if err != nil { 215 | return err 216 | } 217 | 218 | // write the schema 219 | schemaFile := path.Join(versionPath, "schema.cue") 220 | return os.WriteFile(schemaFile, schemaCue, 0o755) 221 | } 222 | 223 | // AddVersion creates a new version for the specified 224 | // schema 225 | func (r *Repository) AddVersion(schema string) error { 226 | var sch *Schema 227 | for _, s := range r.Schemas { 228 | if s.Name == schema { 229 | sch = s 230 | } 231 | } 232 | if sch == nil { 233 | return fmt.Errorf("schema %s not found", schema) 234 | } 235 | versions := len(sch.Versions) 236 | prevVersionPath := path.Join(r.Root, sch.Name, fmt.Sprintf("v%d", versions)) 237 | pterm.Info.Printf("Schema %s has %d version(s)\n", sch.Name, versions) 238 | nextVersion := versions + 1 239 | nextVersionPath := path.Join(r.Root, sch.Name, fmt.Sprintf("v%d", nextVersion)) 240 | err := os.MkdirAll(nextVersionPath, 0o755) 241 | if err != nil { 242 | return err 243 | } 244 | err = copy.Copy(prevVersionPath, nextVersionPath) 245 | if err != nil { 246 | return err 247 | } 248 | return nil 249 | } 250 | 251 | // Build serializes the Repository object 252 | // into a json file in the `Output` directory. 253 | func (r *Repository) Build() error { 254 | pterm.Debug.Printf("\t\tBuilding repository to %s\n", r.Output) 255 | buildDir := path.Join(r.Root, r.Output) 256 | buildFile := path.Join(buildDir, "manifest.json") 257 | 258 | err := os.MkdirAll(buildDir, 0o755) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | bb, err := json.Marshal(r) 264 | if err != nil { 265 | return err 266 | } 267 | err = os.WriteFile(buildFile, bb, 0o755) 268 | if err != nil { 269 | return err 270 | } 271 | pterm.Debug.Printf("\t\tManifest written to %s\n", buildFile) 272 | 273 | return nil 274 | } 275 | -------------------------------------------------------------------------------- /internal/repository/schema.cue: -------------------------------------------------------------------------------- 1 | { 2 | // No "version", we expect people to use the path 3 | // of the schema to version 4 | _schema: { 5 | name: "blox" 6 | namespace: "schemas.cueblox.com" 7 | } 8 | 9 | #Profile: { 10 | _model: { 11 | // Lets assume lowercase Profile is ID 12 | // Lets assume lowercase Profile with _id is the foreign key 13 | // Plural for directory name 14 | plural: "profiles" 15 | } 16 | 17 | name: #Name 18 | address: #Address 19 | } 20 | 21 | #Name: { 22 | forename: string 23 | surname: string 24 | } 25 | 26 | #Address: { 27 | number: string 28 | street: string 29 | city: string 30 | country: string 31 | postcode: string 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/repository/schema.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | // Schema is a definition of a set of 4 | // related models 5 | type Schema struct { 6 | Namespace string 7 | Name string 8 | Versions []*Version 9 | } 10 | -------------------------------------------------------------------------------- /internal/repository/version.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | // Version represents a version of a schema 4 | type Version struct { 5 | Namespace string 6 | Name string 7 | Schema string 8 | Definition string 9 | } 10 | -------------------------------------------------------------------------------- /pkgs/blox/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | self, 3 | buildGoModule, 4 | lib, 5 | }: 6 | buildGoModule { 7 | pname = "blox"; 8 | version = "0.7.5"; 9 | src = self; # + "/src"; 10 | # vendorSha256 should be set to null if dependencies are vendored. If the dependencies aren't 11 | # vendored, vendorSha256 must be set to a hash of the content of all dependencies. This hash can 12 | # be found by setting 13 | # vendorSha256 = lib.fakeSha256; 14 | # and then running flox build. The build will fail but output the expected sha, which can then be 15 | # added here. 16 | vendorSha256 = "sha256-fpDNEjJdTtLuy6DQqkwaVcEOW2GzYkEvLSWZ/qfWNqE="; 17 | } 18 | -------------------------------------------------------------------------------- /plugins/postbuild_interface.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "net/rpc" 5 | 6 | "github.com/hashicorp/go-plugin" 7 | ) 8 | 9 | // Postbuild is the interface that we're exposing as a plugin. 10 | type Postbuild interface { 11 | Process(bloxConfig string) error 12 | } 13 | 14 | // Here is an implementation that talks over RPC 15 | type PostbuildRPC struct{ client *rpc.Client } 16 | 17 | func (g *PostbuildRPC) Process(bloxConfig string) error { 18 | var resp error 19 | err := g.client.Call("Plugin.Process", bloxConfig, &resp) 20 | if err != nil { 21 | // You usually want your interfaces to return errors. If they don't, 22 | // there isn't much other choice here. 23 | panic(err) 24 | } 25 | 26 | return resp 27 | } 28 | 29 | // Here is the RPC server that GreeterRPC talks to, conforming to 30 | // the requirements of net/rpc 31 | type PostbuildRPCServer struct { 32 | // This is the real implementation 33 | Impl Postbuild 34 | } 35 | 36 | func (s *PostbuildRPCServer) Process(bloxConfig string, resp *error) error { 37 | *resp = s.Impl.Process(bloxConfig) 38 | return nil 39 | } 40 | 41 | // This is the implementation of plugin.Plugin so we can serve/consume this 42 | // 43 | // This has two methods: Server must return an RPC server for this plugin 44 | // type. We construct a GreeterRPCServer for this. 45 | // 46 | // Client must return an implementation of our interface that communicates 47 | // over an RPC client. We return GreeterRPC for this. 48 | // 49 | // Ignore MuxBroker. That is used to create more multiplexed streams on our 50 | // plugin connection and is a more advanced use case. 51 | type PostbuildPlugin struct { 52 | // Impl Injection 53 | Impl Postbuild 54 | } 55 | 56 | func (p *PostbuildPlugin) Server(*plugin.MuxBroker) (interface{}, error) { 57 | return &PrebuildRPCServer{Impl: p.Impl}, nil 58 | } 59 | 60 | func (PostbuildPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { 61 | return &PostbuildRPC{client: c}, nil 62 | } 63 | -------------------------------------------------------------------------------- /plugins/prebuild_interface.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "net/rpc" 5 | 6 | "github.com/hashicorp/go-plugin" 7 | ) 8 | 9 | // Prebuild is the interface that we're exposing as a plugin. 10 | type Prebuild interface { 11 | Process(bloxConfig string) error 12 | } 13 | 14 | // Here is an implementation that talks over RPC 15 | type PrebuildRPC struct{ client *rpc.Client } 16 | 17 | func (g *PrebuildRPC) Process(bloxConfig string) error { 18 | var resp error 19 | err := g.client.Call("Plugin.Process", bloxConfig, &resp) 20 | if err != nil { 21 | // You usually want your interfaces to return errors. If they don't, 22 | // there isn't much other choice here. 23 | panic(err) 24 | } 25 | 26 | return resp 27 | } 28 | 29 | // Here is the RPC server that GreeterRPC talks to, conforming to 30 | // the requirements of net/rpc 31 | type PrebuildRPCServer struct { 32 | // This is the real implementation 33 | Impl Prebuild 34 | } 35 | 36 | func (s *PrebuildRPCServer) Process(bloxConfig string, resp *error) error { 37 | *resp = s.Impl.Process(bloxConfig) 38 | return nil 39 | } 40 | 41 | // This is the implementation of plugin.Plugin so we can serve/consume this 42 | // 43 | // This has two methods: Server must return an RPC server for this plugin 44 | // type. We construct a GreeterRPCServer for this. 45 | // 46 | // Client must return an implementation of our interface that communicates 47 | // over an RPC client. We return GreeterRPC for this. 48 | // 49 | // Ignore MuxBroker. That is used to create more multiplexed streams on our 50 | // plugin connection and is a more advanced use case. 51 | type PrebuildPlugin struct { 52 | // Impl Injection 53 | Impl Prebuild 54 | } 55 | 56 | func (p *PrebuildPlugin) Server(*plugin.MuxBroker) (interface{}, error) { 57 | return &PrebuildRPCServer{Impl: p.Impl}, nil 58 | } 59 | 60 | func (PrebuildPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { 61 | return &PrebuildRPC{client: c}, nil 62 | } 63 | -------------------------------------------------------------------------------- /plugins/shared/post.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import "github.com/hashicorp/go-plugin" 4 | 5 | // PostbuildHandshakeConfigs are used to just do a basic handshake between 6 | // a plugin and host. If the handshake fails, a user friendly error is shown. 7 | // This prevents users from executing bad plugins or executing a plugin 8 | // directory. It is a UX feature, not a security feature. 9 | var PostbuildHandshakeConfig = plugin.HandshakeConfig{ 10 | ProtocolVersion: 1, 11 | MagicCookieKey: "BLOX_PLUGIN", 12 | MagicCookieValue: "postbuild", 13 | } 14 | -------------------------------------------------------------------------------- /plugins/shared/pre.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import "github.com/hashicorp/go-plugin" 4 | 5 | // PrebuildHandshakeConfigs are used to just do a basic handshake between 6 | // a plugin and host. If the handshake fails, a user friendly error is shown. 7 | // This prevents users from executing bad plugins or executing a plugin 8 | // directory. It is a UX feature, not a security feature. 9 | var PrebuildHandshakeConfig = plugin.HandshakeConfig{ 10 | ProtocolVersion: 1, 11 | MagicCookieKey: "BLOX_PLUGIN", 12 | MagicCookieValue: "prebuild", 13 | } 14 | -------------------------------------------------------------------------------- /result: -------------------------------------------------------------------------------- 1 | /nix/store/6hfzzcdy7w04hfbmxvfkvj5g6apf6698-blox-0.0.0 -------------------------------------------------------------------------------- /runtime.go: -------------------------------------------------------------------------------- 1 | package blox 2 | 3 | import ( 4 | "cuelang.org/go/cue" 5 | "cuelang.org/go/cue/cuecontext" 6 | ) 7 | 8 | type Runtime struct { 9 | CueContext *cue.Context 10 | Database cue.Value 11 | } 12 | 13 | // NewRuntime creates a new runtime engine 14 | func NewRuntime() (*Runtime, error) { 15 | cueContext := cuecontext.New() 16 | cueValue := cueContext.CompileString("") 17 | 18 | if cueValue.Err() != nil { 19 | return nil, cueValue.Err() 20 | } 21 | 22 | runtime := &Runtime{ 23 | CueContext: cueContext, 24 | Database: cueValue, 25 | } 26 | 27 | return runtime, nil 28 | } 29 | 30 | // NewRuntimeWithBase creates a new runtime engine 31 | // with the cue provided in `base` as the initial cue values 32 | func NewRuntimeWithBase(base string) (*Runtime, error) { 33 | cueContext := cuecontext.New() 34 | cueValue := cueContext.CompileString(base) 35 | 36 | if cueValue.Err() != nil { 37 | return nil, cueValue.Err() 38 | } 39 | 40 | runtime := &Runtime{ 41 | CueContext: cueContext, 42 | Database: cueValue, 43 | } 44 | 45 | return runtime, nil 46 | } 47 | -------------------------------------------------------------------------------- /scripts/cmd_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | SED="sed" 5 | if which gsed >/dev/null 2>&1; then 6 | SED="gsed" 7 | fi 8 | 9 | rm -rf dogfood/sites/docs/docs/cmd/ 10 | mkdir -p dogfood/sites/docs/docs/cmd/ 11 | go run ./cmd/blox docs 12 | "$SED" \ 13 | -i'' \ 14 | -e 's/SEE ALSO/See also/g' \ 15 | -e 's/^## /# /g' \ 16 | -e 's/^### /## /g' \ 17 | -e 's/^#### /### /g' \ 18 | -e 's/^##### /#### /g' \ 19 | ./dogfood/sites/docs/docs/cmd/*.md 20 | -------------------------------------------------------------------------------- /scripts/completions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | rm -rf completions 4 | mkdir completions 5 | for sh in bash zsh fish; do 6 | go run ./cmd/blox completion "$sh" >"completions/blox.$sh" 7 | done 8 | --------------------------------------------------------------------------------