├── .dockerignore ├── .github └── workflows │ ├── build-base-builder-docker.yaml │ ├── build-base-runner-docker.yaml │ ├── build-main-docker.yaml │ ├── build-main.yaml │ ├── build-release-docker.yaml │ └── build-release.yaml ├── .gitignore ├── .gitmodules ├── .graphqlconfig ├── BETTER.md ├── DEVELOPER.CN.md ├── DEVELOPER.EN.md ├── Dockerfile ├── FEATURE.md ├── LICENSE ├── Makefile ├── README.md ├── STRUCTURE.pdf ├── SUMMARY.md ├── app ├── docs │ ├── docs.go │ ├── swagger.json │ └── swagger.yaml └── main.go ├── assets ├── asset.go └── dbml │ ├── index.html │ └── static │ ├── css │ └── app.2b89c1fa80c039002858.css │ └── js │ ├── 0.3fa0cf4719569bbfb9cd.js │ ├── 2.4845651b00663ff902a4.js │ ├── chargebee.js │ ├── cookie.js │ └── manifest.e3348c2b98c872770741.js ├── base.builder.Dockerfile ├── base.runner.Dockerfile ├── cmd ├── build.go ├── dev.go ├── root.go └── start.go ├── docker ├── Dockerfile.amd64 ├── Dockerfile.arm64 ├── FIREBOOM_SERVER_AND_NODEHOOKS_Dockerfile ├── FIREBOOM_SERVER_Dockerfile ├── GOALNG_SERVER_Dockerfile ├── NODEHOOKS_Dockerfile ├── NODE_SERVER_Dockerfile └── docker-compose.yaml ├── entrypoint.sh ├── go.mod ├── go.sum ├── pkg ├── api │ ├── base │ │ ├── handler.go │ │ └── router.go │ ├── datasource.go │ ├── engine.go │ ├── env.go │ ├── global_operation.go │ ├── home.go │ ├── operation.go │ ├── sdk.go │ ├── storage.go │ └── system.go ├── common │ ├── configs │ │ ├── env.go │ │ ├── global_setting.go │ │ ├── jaeger.go │ │ ├── license.go │ │ ├── license_test.go │ │ ├── logger.go │ │ ├── logger_collector.go │ │ ├── logger_file.go │ │ ├── logger_hooked.go │ │ ├── resource.go │ │ └── websocket.go │ ├── consts │ │ ├── config_path.go │ │ ├── engine_hook.go │ │ ├── env_param.go │ │ ├── graphql_key.go │ │ ├── license.go │ │ └── request_param.go │ ├── models │ │ ├── authentication.go │ │ ├── datasource.go │ │ ├── datasource_customize.go │ │ ├── datasource_uploadfile.go │ │ ├── global_operation.go │ │ ├── global_operation_hook.go │ │ ├── hook.go │ │ ├── operation.go │ │ ├── operation_extension.go │ │ ├── operation_graphql.go │ │ ├── operation_graphql_history.go │ │ ├── operation_hook.go │ │ ├── plugin_test.go │ │ ├── role.go │ │ ├── sdk.go │ │ ├── sdk_extension.go │ │ ├── sdk_git.go │ │ ├── storage.go │ │ ├── storage_client.go │ │ └── storage_profile_hook.go │ └── utils │ │ ├── date.go │ │ ├── engine.go │ │ ├── file.go │ │ ├── http.go │ │ ├── init_method.go │ │ ├── jsonschema.go │ │ ├── license.go │ │ ├── path.go │ │ ├── string.go │ │ ├── swagger.go │ │ ├── sync_map.go │ │ ├── validate_value.go │ │ ├── viper_lock.go │ │ └── zip.go ├── engine │ ├── asyncapi │ │ ├── binding.go │ │ ├── binding_channel.go │ │ ├── binding_message.go │ │ ├── binding_operation.go │ │ ├── binding_server.go │ │ ├── channel.go │ │ ├── component.go │ │ ├── message.go │ │ ├── message_trait.go │ │ ├── operation.go │ │ ├── operation_reply.go │ │ ├── operation_trait.go │ │ ├── securityschema.go │ │ ├── server.go │ │ └── spec.go │ ├── build │ │ ├── authentication_config.go │ │ ├── common.go │ │ ├── engine_configuration.go │ │ ├── engine_configuration_rename.go │ │ ├── operations.go │ │ ├── operations_eventbus.go │ │ ├── operations_extension.go │ │ ├── operations_graphql.go │ │ ├── operations_graphql_document.go │ │ ├── upload_configuration.go │ │ ├── upload_configuration_eventbus.go │ │ └── webhooks.go │ ├── datasource │ │ ├── asyncapi.go │ │ ├── common.go │ │ ├── common_cache.go │ │ ├── database.go │ │ ├── graphql.go │ │ ├── graphql_schemaformatter.go │ │ ├── openapi.go │ │ ├── openapi_datasourceconfiguration.go │ │ ├── openapi_graphqlschema.go │ │ ├── openapi_graphqlschema_visitor.go │ │ ├── prisma.go │ │ ├── prisma_engine.go │ │ ├── prisma_engine_query.go │ │ ├── prisma_engine_schema.go │ │ └── prisma_extend.go │ ├── directives │ │ ├── common.go │ │ ├── operation_disallowparallel.go │ │ ├── operation_internaloperation.go │ │ ├── operation_rbac.go │ │ ├── operation_transaction.go │ │ ├── selection_asyncresolve.go │ │ ├── selection_customizedfield.go │ │ ├── selection_export.go │ │ ├── selection_exportmatch.go │ │ ├── selection_firstRawResult.go │ │ ├── selection_formatdatetime.go │ │ ├── selection_include.go │ │ ├── selection_skip.go │ │ ├── selection_skipvariable.go │ │ ├── selection_transform.go │ │ ├── variable_fromclaim.go │ │ ├── variable_fromheader.go │ │ ├── variable_hookvariable.go │ │ ├── variable_injectcurrentdatetime.go │ │ ├── variable_injectenvironmentvariable.go │ │ ├── variable_injectgenerateduuid.go │ │ ├── variable_injectrulevalue.go │ │ ├── variable_internal.go │ │ ├── variable_jsonschema.go │ │ └── variable_whereinput.go │ ├── sdk │ │ ├── hook_reflect.go │ │ ├── hook_swagger.go │ │ ├── schema.go │ │ ├── template.go │ │ ├── template_eventbus.go │ │ └── template_helpers.go │ ├── server │ │ ├── build.go │ │ ├── build_eventbus.go │ │ ├── start.go │ │ └── start_eventbus.go │ └── swagger │ │ ├── api_authentication.go │ │ ├── api_operation.go │ │ ├── api_upload.go │ │ └── document.go ├── plugins │ ├── embed │ │ ├── code.go │ │ ├── datasource │ │ │ └── system.json │ │ ├── default │ │ │ ├── .env │ │ │ ├── .env.prod │ │ │ ├── global.operation.json │ │ │ ├── global.setting.json │ │ │ ├── jaeger.config.yml │ │ │ └── license.json │ │ ├── directive_example │ │ │ ├── operation_internaloperation.graphql │ │ │ ├── operation_rbac.graphql │ │ │ ├── operation_transaction.graphql │ │ │ ├── selection_export.graphql │ │ │ ├── selection_formatdatetime.graphql │ │ │ ├── selection_transform.graphql │ │ │ ├── variable_fromclaim.graphql │ │ │ ├── variable_fromheader.graphql │ │ │ ├── variable_hookvariable.graphql │ │ │ ├── variable_injectcurrentdatetime.graphql │ │ │ ├── variable_injectenvironmentvariable.graphql │ │ │ ├── variable_injectgenerateduuid.graphql │ │ │ ├── variable_internal.graphql │ │ │ ├── variable_jsonschema.graphql │ │ │ └── variable_whereinput.graphql │ │ ├── resource │ │ │ ├── application.properties │ │ │ ├── banner.txt │ │ │ └── introspect.json │ │ └── role │ │ │ ├── admin.json │ │ │ └── user.json │ ├── fileloader │ │ ├── extension.go │ │ ├── model.go │ │ ├── model_data_hook.go │ │ ├── model_data_lock.go │ │ ├── model_data_modify.go │ │ ├── model_data_rw.go │ │ ├── model_data_tree.go │ │ ├── model_data_watcher.go │ │ ├── model_private.go │ │ ├── model_text.go │ │ ├── model_text_data_rw.go │ │ ├── model_text_private.go │ │ └── util.go │ └── i18n │ │ ├── directive.go │ │ ├── directive │ │ └── zh_cn │ │ │ ├── operation_disallowparallel.toml │ │ │ ├── operation_internaloperation.toml │ │ │ ├── operation_rbac.toml │ │ │ ├── operation_transaction.toml │ │ │ ├── selection_asyncresolve.toml │ │ │ ├── selection_customizedfield.toml │ │ │ ├── selection_export.toml │ │ │ ├── selection_exportmatch.toml │ │ │ ├── selection_firstrawresult.toml │ │ │ ├── selection_formatdatetime.toml │ │ │ ├── selection_skipvariable.toml │ │ │ ├── selection_transform.toml │ │ │ ├── variable_fromclaim.toml │ │ │ ├── variable_fromheader.toml │ │ │ ├── variable_hookvariable.toml │ │ │ ├── variable_injectcurrentdatetime.toml │ │ │ ├── variable_injectenvironmentvariable.toml │ │ │ ├── variable_injectgenerateduuid.toml │ │ │ ├── variable_injectruleValue.toml │ │ │ ├── variable_internal.toml │ │ │ ├── variable_jsonschema.toml │ │ │ └── variable_whereinput.toml │ │ ├── directive_i18n_string.go │ │ ├── errcode.go │ │ ├── errcode │ │ └── zh_cn │ │ │ ├── data.toml │ │ │ ├── datasource.toml │ │ │ ├── engine.toml │ │ │ ├── file.toml │ │ │ ├── fileloader.toml │ │ │ ├── operation.toml │ │ │ ├── param.toml │ │ │ ├── request.toml │ │ │ ├── sdk.toml │ │ │ ├── server.toml │ │ │ ├── setting.toml │ │ │ ├── storage.toml │ │ │ └── vscode.toml │ │ ├── errcode_i18n_string.go │ │ ├── prisma_error.go │ │ ├── prisma_error │ │ └── zh_cn │ │ │ ├── common.toml │ │ │ ├── query_engine.toml │ │ │ └── schema_engine.toml │ │ └── prismaerror_i18n_string.go ├── server │ ├── embed_data.go │ ├── middleware.go │ ├── router.go │ ├── run.go │ └── swagger.go ├── vscode │ ├── handler.go │ ├── model.go │ └── router.go └── websocket │ ├── core_server.go │ ├── engine_info.go │ ├── eventbus_notice.go │ ├── hook_report.go │ ├── license.go │ ├── question.go │ ├── question_eventbus.go │ └── web_container.go ├── restart.sh └── scripts ├── build-all.sh ├── build.sh ├── clear-bin.sh ├── export-commit.sh ├── pull-submodule.sh ├── skip-front.sh ├── tag-all.sh └── tar-all.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | custom-go 3 | custom-python 4 | custom-ts 5 | custom-java 6 | exported 7 | docker 8 | generated-sdk 9 | log 10 | release 11 | store/ 12 | template/ 13 | upload/ 14 | .env 15 | .env.* 16 | authentication.key 17 | license.key 18 | Dockerfile 19 | *.Dockerfile -------------------------------------------------------------------------------- /.github/workflows/build-base-builder-docker.yaml: -------------------------------------------------------------------------------- 1 | name: Build base builder docker image 2 | on: 3 | push: 4 | paths: 5 | - base.builder.Dockerfile 6 | - ./github/workflows/build-base-builder-docker.yaml 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up QEMU 14 | uses: docker/setup-qemu-action@v3 15 | - name: Set up Docker Buildx 16 | uses: docker/setup-buildx-action@v3 17 | - name: Log in to Docker Hub 18 | uses: docker/login-action@v3 19 | with: 20 | username: ${{ secrets.DOCKERHUB_USERNAME }} 21 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 22 | - name: Build and push 23 | uses: docker/build-push-action@v5 24 | with: 25 | push: true 26 | file: base.builder.Dockerfile 27 | context: . 28 | platforms: linux/amd64,linux/arm64 29 | tags: fireboomapi/base-builder-fireboom:latest -------------------------------------------------------------------------------- /.github/workflows/build-base-runner-docker.yaml: -------------------------------------------------------------------------------- 1 | name: Build base runner docker image 2 | on: 3 | push: 4 | paths: 5 | - base.runner.Dockerfile 6 | - ./github/workflows/build-base-runner-docker.yaml 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up QEMU 14 | uses: docker/setup-qemu-action@v3 15 | - name: Set up Docker Buildx 16 | uses: docker/setup-buildx-action@v3 17 | - name: Log in to Docker Hub 18 | uses: docker/login-action@v3 19 | with: 20 | username: ${{ secrets.DOCKERHUB_USERNAME }} 21 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 22 | - name: Build and push 23 | uses: docker/build-push-action@v5 24 | with: 25 | push: true 26 | file: base.runner.Dockerfile 27 | context: . 28 | platforms: linux/amd64,linux/arm64 29 | tags: fireboomapi/base-runner-fireboom:latest -------------------------------------------------------------------------------- /.github/workflows/build-main-docker.yaml: -------------------------------------------------------------------------------- 1 | name: Build fireboom latest docker image from main branch 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths_ignore: 7 | - './github/workflows/build-base-builder-docker.yaml' 8 | - './github/workflows/build-base-runner-docker.yaml' 9 | - './github/workflows/build-release-docker.yaml' 10 | - './github/workflows/build-release.yaml' 11 | - './github/workflows/build-main.yaml' 12 | - 'README.md' 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | env: 18 | FB_VERSION: test 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | submodules: recursive 23 | - name: Set up QEMU 24 | uses: docker/setup-qemu-action@v3 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | - name: Log in to Docker Hub 28 | uses: docker/login-action@v3 29 | with: 30 | username: ${{ secrets.DOCKERHUB_USERNAME }} 31 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 32 | - name: Build and push 33 | uses: docker/build-push-action@v5 34 | with: 35 | context: . 36 | push: true 37 | platforms: linux/amd64,linux/arm64 38 | tags: fireboomapi/fireboom:dev 39 | cache-from: type=gha 40 | cache-to: type=gha,mode=max 41 | build-without-web: 42 | runs-on: ubuntu-latest 43 | env: 44 | FB_VERSION: test 45 | steps: 46 | - uses: actions/checkout@v4 47 | with: 48 | submodules: recursive 49 | - name: Set up QEMU 50 | uses: docker/setup-qemu-action@v3 51 | - name: Set up Docker Buildx 52 | uses: docker/setup-buildx-action@v3 53 | - name: Log in to Docker Hub 54 | uses: docker/login-action@v3 55 | with: 56 | username: ${{ secrets.DOCKERHUB_USERNAME }} 57 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 58 | - name: Remove frontend 59 | run: sh scripts/skip-front.sh 60 | - name: Build and push 61 | uses: docker/build-push-action@v5 62 | with: 63 | context: . 64 | push: true 65 | platforms: linux/amd64,linux/arm64 66 | tags: fireboomapi/fireboom_without-web:dev 67 | cache-from: type=gha 68 | cache-to: type=gha,mode=max 69 | -------------------------------------------------------------------------------- /.github/workflows/build-main.yaml: -------------------------------------------------------------------------------- 1 | name: Build fireboom latest binary from main branch 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths_ignore: 7 | - './github/workflows/build-base-builder-docker.yaml' 8 | - './github/workflows/build-base-runner-docker.yaml' 9 | - './github/workflows/build-release-docker.yaml' 10 | - './github/workflows/build-release.yaml' 11 | - './github/workflows/build-main-docker.yaml' 12 | - 'Dockerfile' 13 | - 'README.md' 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | env: 18 | FB_VERSION: test 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | submodules: recursive 23 | - name: Set up Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: 1.21.8 27 | - name: Build 28 | run: | 29 | go mod tidy 30 | sh scripts/build-all.sh 31 | sh scripts/tar-all.sh 32 | sh scripts/clear-bin.sh 33 | - name: R2 Upload Action 34 | uses: ryand56/r2-upload-action@latest 35 | with: 36 | r2-account-id: ${{ secrets.R2_ACCOUNT_ID }} 37 | r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }} 38 | r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }} 39 | r2-bucket: fb-bin 40 | source-dir: release 41 | destination-dir: ./main 42 | build-without-web: 43 | runs-on: ubuntu-latest 44 | env: 45 | FB_VERSION: test 46 | BIN_SUFFIX: _without-web 47 | steps: 48 | - uses: actions/checkout@v4 49 | with: 50 | submodules: recursive 51 | - name: Set up Go 52 | uses: actions/setup-go@v5 53 | with: 54 | go-version: 1.21.8 55 | - name: Build 56 | run: | 57 | go mod tidy 58 | sh scripts/skip-front.sh 59 | sh scripts/build-all.sh 60 | sh scripts/tar-all.sh 61 | sh scripts/clear-bin.sh 62 | - name: R2 Upload Action 63 | uses: ryand56/r2-upload-action@latest 64 | with: 65 | r2-account-id: ${{ secrets.R2_ACCOUNT_ID }} 66 | r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }} 67 | r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }} 68 | r2-bucket: fb-bin 69 | source-dir: release 70 | destination-dir: ./main 71 | -------------------------------------------------------------------------------- /.github/workflows/build-release-docker.yaml: -------------------------------------------------------------------------------- 1 | name: Build docker production image for release branch 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | submodules: recursive 14 | - name: Set up QEMU 15 | uses: docker/setup-qemu-action@v3 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v3 18 | - name: Log in to Docker Hub 19 | uses: docker/login-action@v3 20 | with: 21 | username: ${{ secrets.DOCKERHUB_USERNAME }} 22 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 23 | - name: Build and push 24 | uses: docker/build-push-action@v5 25 | env: 26 | FB_VERSION: ${{ github.ref_name }} 27 | with: 28 | context: . 29 | push: true 30 | platforms: linux/amd64,linux/arm64 31 | tags: | 32 | fireboomapi/fireboom:${{ github.ref_name }} 33 | fireboomapi/fireboom:latest 34 | cache-from: type=gha 35 | cache-to: type=gha,mode=max 36 | build-without-web: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v4 40 | with: 41 | submodules: recursive 42 | - name: Set up QEMU 43 | uses: docker/setup-qemu-action@v3 44 | - name: Set up Docker Buildx 45 | uses: docker/setup-buildx-action@v3 46 | - name: Log in to Docker Hub 47 | uses: docker/login-action@v3 48 | with: 49 | username: ${{ secrets.DOCKERHUB_USERNAME }} 50 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 51 | - name: Remove frontend 52 | run: sh scripts/skip-front.sh 53 | - name: Build and push 54 | uses: docker/build-push-action@v5 55 | env: 56 | FB_VERSION: ${{ github.ref_name }} 57 | with: 58 | context: . 59 | push: true 60 | platforms: linux/amd64,linux/arm64 61 | tags: | 62 | fireboomapi/fireboom_without-web:${{ github.ref_name }} 63 | fireboomapi/fireboom_without-web:latest 64 | cache-from: type=gha 65 | cache-to: type=gha,mode=max -------------------------------------------------------------------------------- /.github/workflows/build-release.yaml: -------------------------------------------------------------------------------- 1 | name: Build fireboom production binary from release branch 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | env: 11 | FB_VERSION: ${{ github.ref_name }} 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | submodules: recursive 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: 1.21.8 20 | - name: Build 21 | run: | 22 | go mod tidy 23 | sh scripts/build-all.sh 24 | sh scripts/tar-all.sh 25 | sh scripts/clear-bin.sh 26 | - name: R2 Upload Action 27 | uses: ryand56/r2-upload-action@latest 28 | with: 29 | r2-account-id: ${{ secrets.R2_ACCOUNT_ID }} 30 | r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }} 31 | r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }} 32 | r2-bucket: fb-bin 33 | source-dir: release 34 | destination-dir: ./prod 35 | - name: R2 Upload Action 36 | uses: ryand56/r2-upload-action@latest 37 | with: 38 | r2-account-id: ${{ secrets.R2_ACCOUNT_ID }} 39 | r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }} 40 | r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }} 41 | r2-bucket: fb-bin 42 | source-dir: release 43 | destination-dir: ./versions/${{ env.FB_VERSION }} 44 | build-without-web: 45 | runs-on: ubuntu-latest 46 | env: 47 | FB_VERSION: ${{ github.ref_name }} 48 | BIN_SUFFIX: _without-web 49 | steps: 50 | - uses: actions/checkout@v4 51 | with: 52 | submodules: recursive 53 | - name: Set up Go 54 | uses: actions/setup-go@v5 55 | with: 56 | go-version: 1.21.8 57 | - name: Build 58 | run: | 59 | go mod tidy 60 | sh scripts/skip-front.sh 61 | sh scripts/build-all.sh 62 | sh scripts/tar-all.sh 63 | sh scripts/clear-bin.sh 64 | - name: R2 Upload Action 65 | uses: ryand56/r2-upload-action@latest 66 | with: 67 | r2-account-id: ${{ secrets.R2_ACCOUNT_ID }} 68 | r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }} 69 | r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }} 70 | r2-bucket: fb-bin 71 | source-dir: release 72 | destination-dir: ./prod 73 | - name: R2 Upload Action 74 | uses: ryand56/r2-upload-action@latest 75 | with: 76 | r2-account-id: ${{ secrets.R2_ACCOUNT_ID }} 77 | r2-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }} 78 | r2-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }} 79 | r2-bucket: fb-bin 80 | source-dir: release 81 | destination-dir: ./versions/${{ env.FB_VERSION }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | .process/ 4 | .DS_Store 5 | 6 | log/ 7 | store/ 8 | template/ 9 | upload/ 10 | exported/ 11 | release/ 12 | 13 | sdk/ 14 | generated-sdk/ 15 | custom-*/ 16 | 17 | # 输出文件 18 | *.out 19 | *.db 20 | *.exe 21 | .env* 22 | 23 | debug 24 | fireboom 25 | authentication.key 26 | license.key 27 | 28 | !.gitkeep 29 | !pkg/** -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "wundergraphGitSubmodule"] 2 | path = wundergraphGitSubmodule 3 | url = https://github.com/fireboomio/wundergraph.git 4 | branch = main 5 | [submodule "assets/front"] 6 | path = assets/front 7 | url = https://github.com/fireboomio/fireboom-web.git 8 | branch = release 9 | -------------------------------------------------------------------------------- /.graphqlconfig: -------------------------------------------------------------------------------- 1 | {"projects":{"app":{"name":"app","schemaPath":"exported/generated/fireboom.app.schema.graphql","extensions":{"endpoints":{"app":{"url":"http://localhost:9991/app/main/graphql","headers":{"user-agent":"FireBoom Client"},"introspect":false}}}}}} -------------------------------------------------------------------------------- /BETTER.md: -------------------------------------------------------------------------------- 1 | ## 开发模式下钩子初始化函数执行时机调整 2 | 3 | 1. 正常流程下,钩子重启会引起飞布重新编译(内省数据源和各种接口/hook) 4 | 2. 钩子中有些初始化执行的函数会放在飞布9991正常可访问时执行(加载/轮询数据) 5 | 3. 但钩子中执行这些初始化操作时飞布可能正处于编译过程中,会导致数据初始化异常 6 | 4. 新的钩子针对开发模式设计了二次确认,仅在第二次health时执行初始化函数 7 | 5. 同时在飞布因钩子重启导致的编译完成后会立马触发一次health,避免钩子二次health等待过长 -------------------------------------------------------------------------------- /DEVELOPER.CN.md: -------------------------------------------------------------------------------- 1 | # Fireboom - 开发者入门指南 2 | 3 | 欢迎来到Fireboom项目!这份入门指南旨在帮助您快速初始化项目并在本地开发环境中运行。 4 | 5 | ### 先决条件 6 | 7 | 在开始之前,请确认您的系统已安装了以下软件: 8 | 9 | - Git 10 | - Go 1.21 或更高版本 11 | 12 | ### 克隆仓库 13 | 14 | 首先,请使用以下命令克隆Fireboom仓库: 15 | 16 | ```shell 17 | git clone --recurse-submodules https://github.com/fireboomio/fireboom.git 18 | ``` 19 | 20 | 这里的`--recurse-submodules`选项非常重要,因为它同时会克隆项目中包含的 `fireboom-web` 和 `wundergraph` 两个子模块。 21 | 22 | ### 子模块 23 | 24 | Fireboom项目包含以下两个子模块: 25 | 26 | 1. **前端资源目录(`assets/front`):** 27 | 28 | 此子模块包含项目所需的所有前端资源。 29 | Git仓库地址:https://github.com/fireboomio/fireboom-web.git 30 | 31 | 2. **后端资源目录(`wundergraphGitSubmodule`):** 32 | 33 | 此子模块包含从其他开源项目中fork的后端资源。 34 | Git仓库地址:https://github.com/fireboomio/wundergraph.git 35 | 36 | 为确保子模块是最新的,请执行以下命令: 37 | 38 | ```shell 39 | git submodule update --init --recursive 40 | ``` 41 | 42 | ### 编译项目 43 | 44 | Fireboom项目使用Cobra构建命令行参数解析。第一个参数是必填的,可以是 `dev`、`start` 或 `build`。您还可以选择额外的参数来覆盖环境变量或切换其他功能。 45 | 46 | 请在项目根目录下运行以下命令来编译项目: 47 | 48 | ```shell 49 | go build -o fireboom 50 | ``` 51 | 52 | ### 运行项目 53 | 54 | 编译完成后,您可以运行项目,使用以下命令之一: 55 | 56 | ```shell 57 | ./fireboom dev 58 | ``` 59 | 60 | 或 61 | 62 | ```shell 63 | ./fireboom start 64 | ``` 65 | 66 | 或 67 | 68 | ```shell 69 | ./fireboom build 70 | ``` 71 | 72 | 您可以根据需要添加其他参数来自定义环境设置或功能开关。 73 | 74 | ### 寻求帮助 75 | 76 | 如果在使用命令时遇到任何问题或需求帮助,请查看Cobra文档或在GitHub仓库上提出issue。 77 | 78 | ### 贡献代码 79 | 80 | 欢迎为Fireboom项目贡献代码!请先详细阅读我们的贡献指南,再开始进行Pull Request。 81 | 82 | ### 许可协议 83 | 84 | 该项目是开源的,并遵循[LICENSE](LICENSE)许可协议。在贡献或使用本项目时,请确保您遵守了许可协议。 85 | 86 | ### 联系方式 87 | 88 | 如果您有任何问题或需要协助,请在GitHub仓库中提出issue,我们的团队将会与您联系。 89 | 90 | 期待您在Fireboom项目中的工作!您的贡献是我们及开源社区的宝贵财富。 -------------------------------------------------------------------------------- /DEVELOPER.EN.md: -------------------------------------------------------------------------------- 1 | # Fireboom - Developer Onboarding 2 | 3 | Welcome to the Fireboom project! This README will guide you through the steps needed to get the project up and running on your local development environment. 4 | 5 | ### Prerequisites 6 | 7 | Before you begin, ensure you have the following installed on your system: 8 | 9 | - Git 10 | - Go 1.21 or later 11 | 12 | ### Cloning the Repository 13 | 14 | To get started, clone the Fireboom repository using the following command: 15 | 16 | ```shell 17 | git clone --recurse-submodules https://github.com/fireboomio/fireboom.git 18 | ``` 19 | 20 | The `--recurse-submodules` flag is essential as it also clones the submodules `fireboom-web` and `wundergraph` that are part of this project. 21 | 22 | ### Submodules 23 | 24 | Fireboom has two submodules: 25 | 26 | 1. **Frontend assets directory (`assets/front`):** 27 | 28 | This submodule contains all the frontend resources necessary for the project. 29 | Git repository: https://github.com/fireboomio/fireboom-web.git 30 | 31 | 2. **Backend resources directory (`wundergraphGitSubmodule`):** 32 | 33 | This submodule contains resources for backend functionality, forked from other open-source projects. 34 | Git repository: https://github.com/fireboomio/wundergraph.git 35 | 36 | To ensure your submodules are up to date, run: 37 | 38 | ```shell 39 | git submodule update --init --recursive 40 | ``` 41 | 42 | ### Building the Project 43 | 44 | Fireboom uses Cobra for command-line argument parsing. The first argument is mandatory and can be one of `dev`, `start`, or `build`. Additional optional arguments can be used to override environment variables or toggle other features. 45 | 46 | To build the project, navigate to the root directory of the project and run: 47 | 48 | ```shell 49 | go build -o fireboom 50 | ``` 51 | 52 | ### Running the Project 53 | 54 | After building, you can run the project using one of the mandatory commands. For example: 55 | 56 | ```shell 57 | ./fireboom dev 58 | ``` 59 | 60 | or 61 | 62 | ```shell 63 | ./fireboom start 64 | ``` 65 | 66 | or 67 | 68 | ```shell 69 | ./fireboom build 70 | ``` 71 | 72 | You can pass additional arguments as needed to customize the environment or functionalities. 73 | 74 | ### Getting Help 75 | 76 | If you need help with the commands or have any issues, you can refer to the Cobra documentation or create an issue in the Fireboom github repository. 77 | 78 | ### Contributing 79 | 80 | We welcome contributions! Please read through our CONTRIBUTING guide to get started on making pull requests. 81 | 82 | ### License 83 | 84 | This project is open-source and is licensed under [LICENSE](LICENSE). Please make sure you comply with the license terms while contributing or using this project. 85 | 86 | ### Contact 87 | 88 | If you have any further questions or require assistance, please open an issue on the Fireboom GitHub repository, and someone from the team will be in touch. 89 | 90 | We hope you enjoy working on Fireboom! Your contributions are valuable to us and the open-source community. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fireboomapi/base-builder-fireboom:latest as builder 2 | COPY . . 3 | ENV GOPROXY=https://proxy.golang.org,direct 4 | RUN sh scripts/build.sh 5 | 6 | FROM fireboomapi/base-runner-fireboom:latest AS final 7 | WORKDIR /app 8 | COPY --from=builder /build/release/fireboom /usr/local/bin/fireboom 9 | #VOLUME [ "store", "upload", "exported", "generated-sdk", "authentication.key", "license.key", "custom-go", "custom-ts", "custom-python", "custom-java" ] 10 | EXPOSE 9123 9991 11 | 12 | # ENV FB_API_PUBLIC_URL="http://localhost:9991" 13 | ENV FB_API_INTERNAL_URL="http://localhost:9991" 14 | ENV FB_API_LISTEN_HOST="0.0.0.0" 15 | ENV FB_API_LISTEN_PORT=9991 16 | ENV FB_SERVER_LISTEN_HOST="localhost" 17 | ENV FB_SERVER_LISTEN_PORT=9992 18 | ENV FB_SERVER_URL="http://localhost:9992" 19 | ENV FB_LOG_LEVEL="DEBUG" 20 | ENV FB_REPO_URL_MIRROR="https://git.fireboom.io/{orgName}/{repoName}.git" 21 | ENV FB_RAW_URL_MIRROR="https://raw.git.fireboom.io/{orgName}/{repoName}/{branchName}/{filePath}" 22 | 23 | ENTRYPOINT [ "fireboom" ] 24 | CMD [ "start" ] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build -o fireboom app/main.go 3 | 4 | build-linux: 5 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o firboom_linux app/main.go 6 | 7 | build-mac: 8 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o firboom_mac app/main.go 9 | 10 | run: 11 | ./fireboom dev 12 | 13 | build-run: 14 | make build && make run 15 | 16 | swagger: 17 | swag init -g app/main.go -o app/docs 18 | 19 | check: 20 | go vet ./... -------------------------------------------------------------------------------- /STRUCTURE.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/fireboom/23d731b087109ed6281fc83eaff225fb82723755/STRUCTURE.pdf -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | ### 1. 产品客户画像 2 | 3 | Fireboom的潜在客户包括: 4 | 5 | - **前端开发者**:通过可视化构建API,前端开发者可以更轻松地参与API开发,实现全栈开发。 6 | - **后端开发者**:声明式API开发、多语言支持、自动生成Swagger文档等功能,使后端开发更高效。 7 | - **独立开发者**:减少接口开发时间,一键部署到Sealos平台,实现分钟级交付,适用于独立开发者或小团队。 8 | - **Hasura和Supabase用户**:提供更灵活的权限系统、多种数据库支持,适合已有经验的开发者。 9 | 10 | ### 2. 解决的痛点 11 | 12 | Fireboom解决了以下痛点: 13 | 14 | - **开发重复性工作**:传统后端开发中,大部分时间花在CRUD接口上,Fireboom通过可视化和自动生成SDK,加速了接口开发,减少了枯燥的工作。 15 | - **全栈开发门槛**:前端开发者可以使用Fireboom进行可视化API构建,降低了全栈开发门槛。 16 | - **BUG和安全问题**:声明式API开发和实时生成Swagger文档可以减少BUG数量,同时支持鉴权等安全功能。 17 | - **多语言支持**:支持多种后端语言,提供了灵活的钩子机制,满足了不同开发者的需求。 18 | - **快速交付**:减少了接口开发时间,实现分钟级交付,适用于快速迭代的项目。 19 | - **中后台应用开发**:结合中后台管理界面,可以实现复杂业务逻辑,覆盖了前端低代码难以实现的需求。 20 | 21 | ### 3. 竞争力 22 | 23 | Fireboom的竞争力体现在以下方面: 24 | 25 | - **可视化API开发**:提供可视化构建API的功能,降低了API开发的技术门槛。 26 | - **多数据源支持**:支持多种数据源,包括数据库、REST API、GraphQL和消息队列,适用于各种应用场景。 27 | - **实时推送**:将GET请求转换为实时查询接口,提供实时推送功能,增强了应用的实时性。 28 | - **SDK生成**:自动生成客户端SDK,减少前端开发工作。 29 | - **文件存储和钩子机制**:集成了S3规范的文件存储,提供了灵活的钩子机制,支持自定义业务逻辑。 30 | - **多语言支持**:适用于不同后端语言,增强了开发者的选择自由度。 31 | 32 | ### 4. 技术壁垒评估 33 | 34 | Fireboom的技术壁垒相对较低,具体评估如下: 35 | 36 | - **可视化开发**:可视化构建API降低了对编程技能的要求,使非技术背景的人员也能参与API开发。 37 | - **多语言支持**:支持多种后端语言,使开发者可以使用自己熟悉的语言编写业务逻辑。 38 | - **SDK生成**:自动生成客户端SDK,减少了前端开发的工作量。 39 | - **文档生成**:实时生成Swagger文档,减少了手工编写文档的工作。 40 | - **开源生态**:Fireboom在GitHub上开源,有社区支持和文档资料可供学习。 41 | 42 | 总体而言,Fireboom通过可视化、多语言支持和SDK自动生成等功能,降低了技术壁垒,使更多人可以快速开发API和应用。 -------------------------------------------------------------------------------- /app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | // Make sure to import swag docs, don't remove 5 | _ "fireboom-server/app/docs" 6 | "fireboom-server/cmd" 7 | ) 8 | 9 | var ( 10 | FbVersion string 11 | FbCommit string 12 | ) 13 | 14 | func main() { 15 | cmd.Execute(FbVersion, FbCommit) 16 | } 17 | -------------------------------------------------------------------------------- /assets/asset.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | //go:embed dbml 11 | var dbmlFiles embed.FS 12 | 13 | //go:embed all:front 14 | var frontFiles embed.FS 15 | 16 | const ( 17 | staticDirname = "static" 18 | embedDbmlDirname = "dbml" 19 | embedFrontDirname = "front" 20 | ) 21 | 22 | func getFileSystem(files embed.FS, dirName string) http.FileSystem { 23 | subFiles, err := fs.Sub(files, dirName) 24 | if err != nil { 25 | panic(err) 26 | } 27 | return http.FS(subFiles) 28 | } 29 | 30 | func GetDBMLFileSystem() http.FileSystem { 31 | return getFileSystem(dbmlFiles, embedDbmlDirname) 32 | } 33 | 34 | func GetFrontFileSystem() http.FileSystem { 35 | fileInfo, _ := os.Stat(staticDirname) 36 | if fileInfo != nil && fileInfo.IsDir() { 37 | return http.FS(os.DirFS(staticDirname)) 38 | } 39 | 40 | return getFileSystem(frontFiles, embedFrontDirname) 41 | } 42 | -------------------------------------------------------------------------------- /assets/dbml/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | dbdiagram.io - Database Relationship Diagrams Design Tool 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 32 | 37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /assets/dbml/static/js/cookie.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | window.Cookie = { 3 | set: function(cname, cvalue, exdays) { 4 | var d = new Date(); 5 | d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); 6 | var expires = "expires=" + d.toUTCString(); 7 | document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; 8 | }, 9 | get: function(cname) { 10 | var name = cname + "="; 11 | var ca = document.cookie.split(';'); 12 | for (var i = 0; i < ca.length; i++) { 13 | var c = ca[i]; 14 | while (c.charAt(0) == ' ') { 15 | c = c.substring(1); 16 | } 17 | if (c.indexOf(name) == 0) { 18 | return c.substring(name.length, c.length); 19 | } 20 | } 21 | return ""; 22 | } 23 | }; -------------------------------------------------------------------------------- /assets/dbml/static/js/manifest.e3348c2b98c872770741.js: -------------------------------------------------------------------------------- 1 | ! function(e) { 2 | function r(r) { 3 | for (var n, l, f = r[0], i = r[1], a = r[2], c = 0, s = []; c < f.length; c++) l = f[c], Object.prototype.hasOwnProperty.call(o, l) && o[l] && s.push(o[l][0]), o[l] = 0; 4 | for (n in i) Object.prototype.hasOwnProperty.call(i, n) && (e[n] = i[n]); 5 | for (p && p(r); s.length;) s.shift()(); 6 | return u.push.apply(u, a || []), t() 7 | } 8 | 9 | function t() { 10 | for (var e, r = 0; r < u.length; r++) { 11 | for (var t = u[r], n = !0, f = 1; f < t.length; f++) { 12 | var i = t[f]; 13 | 0 !== o[i] && (n = !1) 14 | } 15 | n && (u.splice(r--, 1), e = l(l.s = t[0])) 16 | } 17 | return e 18 | } 19 | var n = {}, 20 | o = { 21 | 1: 0 22 | }, 23 | u = []; 24 | 25 | function l(r) { 26 | if (n[r]) return n[r].exports; 27 | var t = n[r] = { 28 | i: r, 29 | l: !1, 30 | exports: {} 31 | }; 32 | return e[r].call(t.exports, t, t.exports, l), t.l = !0, t.exports 33 | } 34 | l.m = e, l.c = n, l.d = function(e, r, t) { 35 | l.o(e, r) || Object.defineProperty(e, r, { 36 | enumerable: !0, 37 | get: t 38 | }) 39 | }, l.r = function(e) { 40 | "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { 41 | value: "Module" 42 | }), Object.defineProperty(e, "__esModule", { 43 | value: !0 44 | }) 45 | }, l.t = function(e, r) { 46 | if (1 & r && (e = l(e)), 8 & r) return e; 47 | if (4 & r && "object" == typeof e && e && e.__esModule) return e; 48 | var t = Object.create(null); 49 | if (l.r(t), Object.defineProperty(t, "default", { 50 | enumerable: !0, 51 | value: e 52 | }), 2 & r && "string" != typeof e) 53 | for (var n in e) l.d(t, n, function(r) { 54 | return e[r] 55 | }.bind(null, n)); 56 | return t 57 | }, l.n = function(e) { 58 | var r = e && e.__esModule ? function() { 59 | return e.default 60 | } : function() { 61 | return e 62 | }; 63 | return l.d(r, "a", r), r 64 | }, l.o = function(e, r) { 65 | return Object.prototype.hasOwnProperty.call(e, r) 66 | }, l.p = "/"; 67 | var f = window.webpackJsonp = window.webpackJsonp || [], 68 | i = f.push.bind(f); 69 | f.push = r, f = f.slice(); 70 | for (var a = 0; a < f.length; a++) r(f[a]); 71 | var p = i; 72 | t() 73 | }([]); -------------------------------------------------------------------------------- /base.builder.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.0-alpine3.19 AS builder 2 | WORKDIR /build 3 | ENV GOPROXY https://goproxy.cn 4 | COPY go.mod go.sum ./ 5 | RUN go mod tidy && apk add --no-cache git -------------------------------------------------------------------------------- /base.runner.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.19 2 | RUN apk add --no-cache curl>=8.6.0-r0 bash ca-certificates git -------------------------------------------------------------------------------- /cmd/build.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fireboom-server/pkg/common/consts" 5 | "fireboom-server/pkg/common/utils" 6 | "fireboom-server/pkg/engine/server" 7 | "go.uber.org/zap" 8 | "sync" 9 | 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | var buildCmd = &cobra.Command{ 15 | Use: "build", 16 | Short: "Build fireboom application", 17 | Long: `Build fireboom application to apply the new configuration`, 18 | Example: `./fireboom build`, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | _ = viper.BindPFlags(cmd.Flags()) 21 | viper.Set(consts.EngineFirstStatus, consts.EngineBuilding) 22 | utils.ExecuteInitMethods() 23 | engineBuildWithWaitGroup() 24 | }, 25 | } 26 | 27 | func engineBuildWithWaitGroup() bool { 28 | var group sync.WaitGroup 29 | if err := server.EngineBuilder.GenerateGraphqlConfig(&group); err != nil { 30 | return false 31 | } 32 | 33 | group.Wait() 34 | zap.L().Info("build success") 35 | return true 36 | } 37 | 38 | func init() { 39 | buildCmd.Flags().String(consts.ActiveMode, consts.DefaultProdActive, "Mode active to run in different environment") 40 | buildCmd.Flags().Bool(consts.IgnoreMergeEnvironment, false, "Whether Ignore merge environment") 41 | rootCmd.AddCommand(buildCmd) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/dev.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fireboom-server/pkg/common/consts" 5 | "fireboom-server/pkg/common/utils" 6 | "fireboom-server/pkg/server" 7 | 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | var devCmd = &cobra.Command{ 13 | Use: "dev", 14 | Short: "Start fireboom in development mode", 15 | Long: `Start the fireboom application in development mode and watch for changes`, 16 | Example: `./fireboom dev`, 17 | Run: func(cmd *cobra.Command, args []string) { 18 | _ = viper.BindPFlags(cmd.Flags()) 19 | viper.Set(consts.DevMode, true) 20 | viper.Set(consts.EnableSwagger, true) 21 | viper.Set(consts.EnableWebConsole, true) 22 | viper.Set(consts.EnableDebugPprof, true) 23 | viper.Set(consts.EngineFirstStatus, consts.EngineBuilding) 24 | utils.ExecuteInitMethods() 25 | if err := server.GenerateAuthenticationKey(); err != nil { 26 | return 27 | } 28 | 29 | server.Run(utils.BuildAndStart) 30 | }, 31 | } 32 | 33 | func init() { 34 | devCmd.Flags().String(consts.WebPort, consts.DefaultWebPort, "Web port to listen on") 35 | devCmd.Flags().String(consts.ActiveMode, "", "Mode active to run in different environment") 36 | devCmd.Flags().Bool(consts.EnableLogicDelete, false, "Whether enable logic delete for multiple data") 37 | devCmd.Flags().Bool(consts.IgnoreMergeEnvironment, false, "Whether Ignore merge environment") 38 | 39 | devCmd.Flags().Bool(consts.EnableAuth, false, "Whether enable auth key on dev mode") 40 | devCmd.Flags().Bool(consts.EnableHookReport, true, "Whether enable hook report on dev mode") 41 | rootCmd.AddCommand(devCmd) 42 | } 43 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fireboom-server/pkg/common/consts" 5 | "os" 6 | 7 | "github.com/spf13/viper" 8 | "go.uber.org/zap" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var rootCmd = &cobra.Command{ 14 | Use: "fireboom", 15 | Short: "Fireboom is the next generation API development platform.", 16 | Long: `Fireboom is the next generation API development platform, flexible and open, multi-language compatible, easy to learn, to Firebase, but no vendor locked. It helps you build production-level WEB apis without having to spend time repeating coding.`, 17 | } 18 | 19 | // Execute adds all child commands to the root command and sets flags appropriately. 20 | // This is called by main.main(). It only needs to happen once to the rootCmd. 21 | func Execute(fbVersion, fbCommit string) { 22 | if fbVersion == "" { 23 | fbVersion = consts.DevMode 24 | } 25 | if fbCommit == "" { 26 | fbCommit = consts.DevMode 27 | } 28 | viper.Set(consts.FbVersion, fbVersion) 29 | viper.Set(consts.FbCommit, fbCommit) 30 | if err := rootCmd.Execute(); err != nil { 31 | zap.S().Error(err) 32 | os.Exit(1) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cmd/start.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fireboom-server/pkg/common/consts" 5 | "fireboom-server/pkg/common/utils" 6 | "fireboom-server/pkg/engine/build" 7 | engineServer "fireboom-server/pkg/engine/server" 8 | "fireboom-server/pkg/server" 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | var startCmd = &cobra.Command{ 14 | Use: "start", 15 | Short: "Start fireboom in production mode", 16 | Long: `Start the fireboom application in production mode without watching`, 17 | Example: `./fireboom start`, 18 | Run: func(cmd *cobra.Command, args []string) { 19 | _ = viper.BindPFlags(cmd.Flags()) 20 | viper.Set(consts.EngineFirstStatus, consts.EngineStarting) 21 | utils.ExecuteInitMethods() 22 | utils.SetWithLockViper(consts.EnableAuth, true, true) 23 | if build.GeneratedJsonLoadErrored() { 24 | return 25 | } 26 | if err := server.GenerateAuthenticationKey(); err != nil { 27 | return 28 | } 29 | server.Run(func() { 30 | if utils.GetBoolWithLockViper(consts.EnableRebuild) && !engineBuildWithWaitGroup() { 31 | return 32 | } 33 | engineServer.EngineStarter.StartNodeServer() 34 | }) 35 | }, 36 | } 37 | 38 | func init() { 39 | startCmd.Flags().String(consts.WebPort, consts.DefaultWebPort, "Web port to listen on") 40 | startCmd.Flags().String(consts.ActiveMode, consts.DefaultProdActive, "Mode active to run in different environment") 41 | startCmd.Flags().Bool(consts.EnableLogicDelete, false, "Whether enable logic delete for multiple data") 42 | startCmd.Flags().Bool(consts.IgnoreMergeEnvironment, true, "Whether Ignore merge environment") 43 | 44 | startCmd.Flags().Bool(consts.EnableSwagger, false, "Whether enable swagger in production") 45 | startCmd.Flags().Bool(consts.EnableRebuild, false, "Whether enable rebuild in production") 46 | startCmd.Flags().Bool(consts.EnableWebConsole, true, "Whether enable web console page in production") 47 | startCmd.Flags().Bool(consts.EnableDebugPprof, false, "Whether enable /debug/pprof router in production") 48 | startCmd.Flags().Bool(consts.RegenerateKey, false, "Whether to renew authentication key in production") 49 | rootCmd.AddCommand(startCmd) 50 | } 51 | -------------------------------------------------------------------------------- /docker/Dockerfile.amd64: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ADD release/fireboom-linux ./fireboom 3 | ENTRYPOINT [ "./fireboom" ] 4 | VOLUME [ "store", "upload", "exported", "generated-sdk", "authentication.key", "license.key" ] 5 | EXPOSE 9123 9991 6 | CMD [ "start" ] -------------------------------------------------------------------------------- /docker/Dockerfile.arm64: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | ADD release/fireboom-linux-arm64 ./fireboom 3 | ENTRYPOINT [ "./fireboom" ] 4 | VOLUME [ "store", "upload", "exported", "generated-sdk", "authentication.key", "license.key" ] 5 | EXPOSE 9123 9991 6 | CMD [ "start" ] -------------------------------------------------------------------------------- /docker/FIREBOOM_SERVER_AND_NODEHOOKS_Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$TARGETPLATFORM golang:1.19-alpine AS builder 2 | ARG TARGETOS TARGETARCH 3 | WORKDIR /build 4 | 5 | ENV GOPROXY https://goproxy.cn 6 | COPY .. /fireboom-server 7 | RUN cd /fireboom-server && \ 8 | go mod tidy && \ 9 | CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /fbcli ./app/main.go 10 | 11 | FROM --platform=$TARGETPLATFORM alpine:3.16 AS final 12 | COPY --from=builder /fbcli / 13 | COPY ../template/node-server /template/node-server 14 | COPY ../custom-ts /custom-ts 15 | COPY ../store /store 16 | COPY ../entrypoint.sh /entrypoint.sh 17 | 18 | RUN echo "http://mirrors.aliyun.com/alpine/edge/main/" > /etc/apk/repositories \ 19 | && echo "http://mirrors.aliyun.com/alpine/edge/community/" >> /etc/apk/repositories \ 20 | && apk update \ 21 | && apk add --no-cache --update nodejs npm \ 22 | && node -v && npm -v \ 23 | && npm config set registry https://registry.npm.taobao.org 24 | 25 | EXPOSE 9123 26 | EXPOSE 9991 27 | EXPOSE 9992 28 | 29 | 30 | 31 | VOLUME ["/custom-go","/custom-ts","/store","/log","/template/node-server"] 32 | 33 | ENTRYPOINT ["sh","/entrypoint.sh"] 34 | -------------------------------------------------------------------------------- /docker/FIREBOOM_SERVER_Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19-alpine AS builder 2 | 3 | WORKDIR /build 4 | 5 | ENV GOPROXY https://goproxy.cn 6 | COPY . /fireboom-server 7 | RUN cd /fireboom-server && \ 8 | go mod tidy && \ 9 | CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o /fbcli ./app/main.go 10 | 11 | FROM alpine:3.16 AS final 12 | COPY --from=builder /fbcli / 13 | COPY template/node-server /template/node-server 14 | COPY custom-ts /custom-ts 15 | COPY store /store 16 | 17 | EXPOSE 9123 18 | EXPOSE 9991 19 | 20 | VOLUME ["/custom-go","/custom-ts","/store","/log","/template/node-server"] 21 | 22 | ARG default_excute_command=dev 23 | ENV excute_command=$default_excute_command 24 | ENTRYPOINT /fbcli $excute_command 25 | -------------------------------------------------------------------------------- /docker/GOALNG_SERVER_Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19-alpine AS builder 2 | 3 | WORKDIR /build 4 | 5 | ENV GOPROXY https://goproxy.cn 6 | EXPOSE 9992 7 | VOLUME ["/custom-go"] 8 | 9 | ENTRYPOINT cd /custom-go && \ 10 | go mod tidy && 11 | go run ./main.go 12 | -------------------------------------------------------------------------------- /docker/NODEHOOKS_Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$TARGETPLATFORM golang:1.19-alpine AS builder 2 | ARG TARGETOS TARGETARCH 3 | WORKDIR /build 4 | 5 | ENV GOPROXY https://goproxy.cn 6 | COPY .. /fireboom-server 7 | RUN cd /fireboom-server && \ 8 | go mod tidy && \ 9 | CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /fbcli ./app/main.go 10 | 11 | FROM --platform=$TARGETPLATFORM alpine:3.16 AS final 12 | COPY --from=builder /fbcli / 13 | COPY ../template/node-server /template/node-server 14 | COPY ../custom-ts /custom-ts 15 | COPY ../store /store 16 | COPY ../entrypoint.sh /entrypoint.sh 17 | 18 | RUN echo "http://mirrors.aliyun.com/alpine/edge/main/" > /etc/apk/repositories \ 19 | && echo "http://mirrors.aliyun.com/alpine/edge/community/" >> /etc/apk/repositories \ 20 | && apk update \ 21 | && apk add --no-cache --update nodejs npm \ 22 | && node -v && npm -v \ 23 | && npm config set registry https://registry.npm.taobao.org 24 | 25 | EXPOSE 9123 26 | EXPOSE 9991 27 | EXPOSE 9992 28 | 29 | 30 | 31 | VOLUME ["/custom-go","/custom-ts","/store","/log","/template/node-server"] 32 | 33 | ENTRYPOINT ["sh","/entrypoint.sh"] 34 | -------------------------------------------------------------------------------- /docker/NODE_SERVER_Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19-alpine AS builderFB 2 | 3 | WORKDIR /build 4 | 5 | ENV GOPROXY https://goproxy.cn 6 | COPY . /fireboom-server 7 | RUN cd /fireboom-server && \ 8 | go mod tidy && \ 9 | CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o /fbcli ./app/main.go 10 | 11 | FROM node:19-alpine3.16 as buildfinal 12 | COPY --from=builderFB /fbcli / 13 | COPY ../template/node-server /template/node-server 14 | COPY ../custom-ts /custom-ts 15 | COPY ../store /store 16 | 17 | EXPOSE 9992 18 | 19 | VOLUME ["/custom-ts","/store"] 20 | 21 | 22 | ENTRYPOINT cd /custom-ts && \ 23 | npm run dev 24 | -------------------------------------------------------------------------------- /docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | app: 4 | image: fireboom:latest 5 | build: 6 | dockerfile: FIREBOOM_SERVER_Dockerfile 7 | context: . 8 | ports: 9 | - 9123:9123 10 | - 9991:9991 11 | depends_on: 12 | - nodehooks 13 | nodehooks: 14 | image: nodehooks:latest 15 | build: 16 | dockerfile: NODE_SERVER_Dockerfile 17 | context: . 18 | ports: 19 | - 9992:9992 -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ### 3 | # @Author: 胡勇超 huyongchao98@163.com 4 | # @Date: 2023-04-26 15:47:00 5 | # @LastEditors: 胡勇超 huyongchao98@163.com 6 | # @LastEditTime: 2023-04-27 21:29:23 7 | # @FilePath: /fireboom-server/entrypoint.sh 8 | # @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE 9 | ### 10 | echo "Staring node server..." 11 | echo '127.0.0.1 localhost.localdomain localhost' > /etc/hosts 12 | cd /custom-ts 13 | npm install 14 | npm run watch & 15 | 16 | while ! echo -e '\x1dclose' | nc localhost 9992 > /dev/null 2>&1 17 | do 18 | echo " node server is not ready yet. Retrying in 5 seconds..." 19 | sleep 5 20 | done 21 | echo "node server is ready. Starting fireboom server..." 22 | cd / 23 | /fbcli dev --host 0.0.0.0 & 24 | wait 25 | -------------------------------------------------------------------------------- /pkg/api/engine.go: -------------------------------------------------------------------------------- 1 | // Package api 2 | /* 3 | 注册引擎相关的路由,包括重启引擎及swagger文档 4 | 飞布提供了两份swagger文档,这里是引擎即9991端口的文档 5 | */ 6 | package api 7 | 8 | import ( 9 | "fireboom-server/pkg/api/base" 10 | "fireboom-server/pkg/common/consts" 11 | "fireboom-server/pkg/common/utils" 12 | "fireboom-server/pkg/engine/build" 13 | "github.com/labstack/echo/v4" 14 | "net/http" 15 | ) 16 | 17 | func EngineRouter(contextRouter *echo.Group) { 18 | handler := &engine{} 19 | engineRouter := contextRouter.Group("/engine") 20 | engineRouter.GET("/restart", handler.restart) 21 | if utils.GetBoolWithLockViper(consts.EnableSwagger) { 22 | engineRouter.GET("/swagger", handler.getSwaggerJsonFile) 23 | } 24 | } 25 | 26 | type engine struct{} 27 | 28 | // @Tags engine 29 | // @Description "引擎swagger.json" 30 | // @Success 200 "成功" 31 | // @Router /engine/swagger [get] 32 | func (s *engine) getSwaggerJsonFile(c echo.Context) error { 33 | base.SetHeaderCacheControlNoCache(c) 34 | return c.File(build.GeneratedSwaggerText.GetPath(build.GeneratedSwaggerText.Title)) 35 | } 36 | 37 | // @Tags engine 38 | // @Description "引擎重启" 39 | // @Success 200 "成功" 40 | // @Router /engine/restart [get] 41 | func (s *engine) restart(c echo.Context) error { 42 | if utils.GetBoolWithLockViper(consts.DevMode) { 43 | go utils.BuildAndStart() 44 | } 45 | return c.NoContent(http.StatusOK) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/api/env.go: -------------------------------------------------------------------------------- 1 | // Package api 2 | /* 3 | 在基础路由上进行扩展 4 | 注册graphql文本,proxy,function定义的路由 5 | 注册开放api接口,角色绑定/解绑,角色权限查询,可以在admin项目中使用 6 | */ 7 | package api 8 | 9 | import ( 10 | "fireboom-server/pkg/api/base" 11 | "fireboom-server/pkg/plugins/fileloader" 12 | "github.com/labstack/echo/v4" 13 | "github.com/subosito/gotenv" 14 | "net/http" 15 | ) 16 | 17 | func EnvExtraRouter(_, envRouter *echo.Group, baseHandler *base.Handler[gotenv.Env], modelRoot *fileloader.Model[gotenv.Env]) { 18 | handler := &env{ 19 | modelRoot.GetModelName(), 20 | modelRoot, 21 | baseHandler, 22 | } 23 | envRouter.GET("/getEnvValue/:key", handler.getEnvValue) 24 | } 25 | 26 | type env struct { 27 | modelName string 28 | modelRoot *fileloader.Model[gotenv.Env] 29 | baseHandler *base.Handler[gotenv.Env] 30 | } 31 | 32 | // @Tags env 33 | // @Description "GetEnvValue" 34 | // @Param key path string true "key" 35 | // @Success 200 {string} string "OK" 36 | // @Router /env/getEnvValue/{key} [get] 37 | func (e *env) getEnvValue(c echo.Context) (err error) { 38 | key, err := e.baseHandler.GetPathParam(c, "key") 39 | if err != nil { 40 | return 41 | } 42 | 43 | return c.String(http.StatusOK, (*e.modelRoot.FirstData())[key]) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/api/global_operation.go: -------------------------------------------------------------------------------- 1 | // Package api 2 | /* 3 | 在基础路由上进行扩展 4 | 注册全局钩子相关的配置路由 5 | */ 6 | package api 7 | 8 | import ( 9 | "fireboom-server/pkg/api/base" 10 | "fireboom-server/pkg/common/models" 11 | "fireboom-server/pkg/plugins/fileloader" 12 | "github.com/labstack/echo/v4" 13 | "net/http" 14 | ) 15 | 16 | func GlobalOperationExtraRouter(_, globalOperationRouter *echo.Group, baseHandler *base.Handler[models.GlobalOperation], modelRoot *fileloader.Model[models.GlobalOperation]) { 17 | handler := &globalOperation{modelRoot.GetModelName(), modelRoot, baseHandler} 18 | globalOperationRouter.GET("/httpTransportHookOptions", handler.httpTransportHookOptions) 19 | globalOperationRouter.GET("/authenticationHookOptions", handler.authenticationHookOptions) 20 | } 21 | 22 | type globalOperation struct { 23 | modelName string 24 | modelRoot *fileloader.Model[models.GlobalOperation] 25 | baseHandler *base.Handler[models.GlobalOperation] 26 | } 27 | 28 | // @Tags globalOperation 29 | // @Description "httpTransportHookOptions" 30 | // @Success 200 {object} models.HookOptions "httpTransportHookOptions配置" 31 | // @Failure 400 {object} i18n.CustomError 32 | // @Router /globalOperation/httpTransportHookOptions [get] 33 | func (o *globalOperation) httpTransportHookOptions(c echo.Context) error { 34 | return c.JSON(http.StatusOK, models.GetHttpTransportHookOptions()) 35 | } 36 | 37 | // @Tags globalOperation 38 | // @Description "authenticationHookOptions" 39 | // @Success 200 {object} models.HookOptions "authenticationHookOptions配置" 40 | // @Failure 400 {object} i18n.CustomError 41 | // @Router /globalOperation/authenticationHookOptions [get] 42 | func (o *globalOperation) authenticationHookOptions(c echo.Context) error { 43 | return c.JSON(http.StatusOK, models.GetAuthenticationHookOptions()) 44 | } 45 | -------------------------------------------------------------------------------- /pkg/api/sdk.go: -------------------------------------------------------------------------------- 1 | // Package api 2 | /* 3 | 在基础路由上进行扩展 4 | 注册服务端钩子查询,打包下载钩子生成目录路由 5 | */ 6 | package api 7 | 8 | import ( 9 | "fireboom-server/pkg/api/base" 10 | "fireboom-server/pkg/common/models" 11 | "fireboom-server/pkg/common/utils" 12 | "fireboom-server/pkg/plugins/fileloader" 13 | "fireboom-server/pkg/plugins/i18n" 14 | "github.com/labstack/echo/v4" 15 | "net/http" 16 | ) 17 | 18 | func SdkRouter(_, sdkRouter *echo.Group, baseHandler *base.Handler[models.Sdk], modelRoot *fileloader.Model[models.Sdk]) { 19 | handler := &sdk{baseHandler, modelRoot, modelRoot.GetModelName()} 20 | sdkRouter.GET("/enabledServer", handler.enabledServer) 21 | sdkRouter.GET("/downloadOutput"+base.DataNamePath, handler.downloadOutput) 22 | } 23 | 24 | type sdk struct { 25 | baseHandler *base.Handler[models.Sdk] 26 | modelRoot *fileloader.Model[models.Sdk] 27 | modelName string 28 | } 29 | 30 | // @Tags sdk 31 | // @Description "开启的服务端钩子" 32 | // @Success 200 {object} models.Sdk "OK" 33 | // @Router /sdk/enabledServer [get] 34 | func (d *sdk) enabledServer(c echo.Context) error { 35 | return c.JSON(http.StatusOK, models.GetEnabledServerSdk()) 36 | } 37 | 38 | // @Description "下载生成output压缩包" 39 | // @Param dataName path string true "dataName" 40 | // @Success 200 "下载成功" 41 | // @Failure 400 {object} i18n.CustomError 42 | // @Router /sdk/downloadOutput/{dataName} [get] 43 | func (d *sdk) downloadOutput(c echo.Context) error { 44 | data, err := d.baseHandler.GetOneByDataName(c) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | zipBuffer, err := utils.ZipFilesWithBuffer([]string{data.OutputPath}, true) 50 | if err != nil { 51 | return i18n.NewCustomErrorWithMode(d.modelName, err, i18n.FileZipError) 52 | } 53 | 54 | base.SetHeaderContentDisposition(c, data.Name+utils.ExtensionZip) 55 | return c.Stream(http.StatusOK, "application/zip", zipBuffer) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/api/system.go: -------------------------------------------------------------------------------- 1 | // Package api 2 | /* 3 | 注册代理路由,为了解决部分用户仓库地址访问不了的问题 4 | 注册目录字典路由,返回所有本地存储文件的目录,依赖于fileload实现 5 | */ 6 | package api 7 | 8 | import ( 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/common/models" 11 | "fireboom-server/pkg/plugins/fileloader" 12 | "fireboom-server/pkg/plugins/i18n" 13 | "github.com/labstack/echo/v4" 14 | "io" 15 | "net/http" 16 | "time" 17 | ) 18 | 19 | func SystemRouter(router *echo.Group) { 20 | handler := &system{} 21 | systemRouter := router.Group("/system") 22 | systemRouter.GET("/proxy", handler.proxyRequest) 23 | systemRouter.GET("/directories", handler.getDirectories) 24 | } 25 | 26 | type system struct{} 27 | 28 | // @Tags system 29 | // @Description "代理请求" 30 | // @Param url query string true "请求url" 31 | // @Success 200 {object} any "成功" 32 | // @Failure 400 {object} i18n.CustomError 33 | // @Router /system/proxy [get] 34 | func (s *system) proxyRequest(c echo.Context) error { 35 | url := c.QueryParam(consts.QueryParamUrl) 36 | if url == "" { 37 | return i18n.NewCustomError(nil, i18n.QueryParamEmptyError, consts.QueryParamUrl) 38 | } 39 | 40 | url = models.ReplaceGithubProxyUrl(url) 41 | client := &http.Client{Transport: &http.Transport{ 42 | MaxIdleConns: 10, 43 | IdleConnTimeout: 30 * time.Second, 44 | DisableCompression: false, 45 | }} 46 | resp, err := client.Get(url) 47 | if err != nil { 48 | return i18n.NewCustomError(err, i18n.RequestProxyError) 49 | } 50 | 51 | if _, err = io.Copy(c.Response().Writer, resp.Body); err != nil { 52 | return i18n.NewCustomError(err, i18n.RequestProxyError) 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // @Tags system 59 | // @Description "获取所有配置目录" 60 | // @Success 200 {object} map[string]string "成功" 61 | // @Failure 400 {object} i18n.CustomError 62 | // @Router /system/directories [get] 63 | func (s *system) getDirectories(c echo.Context) error { 64 | return c.JSON(http.StatusOK, fileloader.GetRootDirectories()) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/common/configs/env.go: -------------------------------------------------------------------------------- 1 | // Package configs 2 | /* 3 | 通过fileloader.ModelText和fileloader.Model来管理.env配置 4 | envDefaultText读取embed内嵌的默认配置 5 | EnvEffectiveRoot读取项目工作目录下的.env配置 6 | envDefaultName在非dev模型下添加.prod后缀,即使用.env.prod作为默认配置 7 | envEffectiveName根据命令行参数--active的值来添加后缀 8 | env变更会触发问题收集和引擎编译重启 9 | */ 10 | package configs 11 | 12 | import ( 13 | "bytes" 14 | "fireboom-server/pkg/common/consts" 15 | "fireboom-server/pkg/common/utils" 16 | "fireboom-server/pkg/plugins/embed" 17 | "fireboom-server/pkg/plugins/fileloader" 18 | "github.com/spf13/viper" 19 | "github.com/subosito/gotenv" 20 | "strings" 21 | ) 22 | 23 | var EnvEffectiveRoot *fileloader.Model[gotenv.Env] 24 | 25 | func init() { 26 | viper.AutomaticEnv() 27 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) 28 | utils.RegisterInitMethod(10, func() { 29 | envDefaultName := consts.DefaultEnv 30 | if !utils.GetBoolWithLockViper(consts.DevMode) { 31 | envDefaultName += utils.StringDot + consts.DefaultProdActive 32 | } 33 | envDefaultText := &fileloader.ModelText[gotenv.Env]{ 34 | Root: embed.DefaultRoot, 35 | ExtensionIgnored: true, 36 | TextRW: &fileloader.EmbedTextRW{ 37 | EmbedFiles: &embed.DefaultFs, 38 | Name: envDefaultName, 39 | }, 40 | } 41 | envDefaultText.Init() 42 | 43 | envEffectiveName := consts.DefaultEnv 44 | if mode := utils.GetStringWithLockViper(consts.ActiveMode); mode != "" { 45 | envEffectiveName += utils.StringDot + mode 46 | } 47 | EnvEffectiveRoot = &fileloader.Model[gotenv.Env]{ 48 | Root: utils.StringDot, 49 | ExtensionIgnored: true, 50 | DataHook: &fileloader.DataHook[gotenv.Env]{ 51 | AfterInit: func(datas map[string]*gotenv.Env) { 52 | for k, v := range *datas[envEffectiveName] { 53 | utils.SetWithLockViper(k, v, true) 54 | } 55 | }, 56 | AfterUpdate: func(data *gotenv.Env, modifies *fileloader.DataModifies, _ string, _ ...string) { 57 | for key, detail := range *modifies { 58 | switch detail.Name { 59 | case fileloader.Add, fileloader.Overwrite: 60 | utils.SetWithLockViper(key, string(detail.Target)) 61 | case fileloader.Remove: 62 | utils.SetWithLockViper(key, nil) 63 | } 64 | } 65 | }, 66 | }, 67 | DataRW: &fileloader.SingleDataRW[gotenv.Env]{ 68 | InitDataBytes: envDefaultText.GetFirstCache(), 69 | IgnoreMergeIfExisted: utils.GetBoolWithLockViper(consts.IgnoreMergeEnvironment), 70 | DataName: envEffectiveName, 71 | Unmarshal: func(dataBytes []byte) (*gotenv.Env, error) { 72 | data := gotenv.Parse(bytes.NewReader(dataBytes)) 73 | return &data, nil 74 | }, 75 | Marshal: func(data *gotenv.Env) ([]byte, error) { 76 | envStr, err := gotenv.Marshal(*data) 77 | return []byte(envStr), err 78 | }, 79 | }, 80 | } 81 | EnvEffectiveRoot.Init(lazyLogger) 82 | AddFileLoaderQuestionCollector(EnvEffectiveRoot.GetModelName(), nil) 83 | utils.AddBuildAndStartFuncWatcher(func(f func()) { EnvEffectiveRoot.DataHook.AfterMutate = f }) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/common/configs/jaeger.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import ( 4 | "fireboom-server/pkg/common/consts" 5 | "fireboom-server/pkg/common/utils" 6 | "fireboom-server/pkg/plugins/embed" 7 | "fireboom-server/pkg/plugins/fileloader" 8 | "github.com/ghodss/yaml" 9 | "github.com/opentracing/opentracing-go" 10 | jaegercfg "github.com/uber/jaeger-client-go/config" 11 | jaegerlog "github.com/uber/jaeger-client-go/log" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | type jaegerConfiguration jaegercfg.Configuration 16 | 17 | func (j *jaegerConfiguration) setGlobalTracer() { 18 | yamlConfig := (*jaegercfg.Configuration)(j) 19 | if _, err := yamlConfig.FromEnv(); err != nil { 20 | logger.Error("setGlobalTracer FromEnv failed", zap.Error(err)) 21 | return 22 | } 23 | if yamlConfig.Disabled { 24 | return 25 | } 26 | 27 | yamlConfig.ServiceName = utils.ReplacePlaceholderFromEnv(yamlConfig.ServiceName) 28 | yamlConfig.Gen128Bit = true 29 | tracer, _, err := yamlConfig.NewTracer(jaegercfg.Logger(jaegerlog.StdLogger)) 30 | if err != nil { 31 | logger.Error("setGlobalTracer NewTracer failed", zap.Error(err)) 32 | return 33 | } 34 | 35 | opentracing.SetGlobalTracer(tracer) 36 | } 37 | 38 | func init() { 39 | utils.RegisterInitMethod(15, func() { 40 | jaegerConfigDefaultText := &fileloader.ModelText[jaegerConfiguration]{ 41 | Root: embed.DefaultRoot, 42 | Extension: fileloader.ExtYml, 43 | TextRW: &fileloader.EmbedTextRW{EmbedFiles: &embed.DefaultFs, Name: consts.JaegerConfig}, 44 | } 45 | jaegerConfigDefaultText.Init() 46 | 47 | jaegerConfigRoot := &fileloader.Model[jaegerConfiguration]{ 48 | Root: utils.NormalizePath(consts.RootStore, consts.StoreConfigParent), 49 | Extension: fileloader.ExtYml, 50 | LoadErrorIgnored: true, 51 | DataHook: &fileloader.DataHook[jaegerConfiguration]{ 52 | AfterInit: func(datas map[string]*jaegerConfiguration) { 53 | datas[consts.JaegerConfig].setGlobalTracer() 54 | }, 55 | AfterUpdate: func(data *jaegerConfiguration, _ *fileloader.DataModifies, _ string, _ ...string) { 56 | data.setGlobalTracer() 57 | }, 58 | }, 59 | DataRW: &fileloader.SingleDataRW[jaegerConfiguration]{ 60 | InitDataBytes: jaegerConfigDefaultText.GetFirstCache(), 61 | DataName: consts.JaegerConfig, 62 | Unmarshal: func(dataBytes []byte) (data *jaegerConfiguration, err error) { 63 | err = yaml.Unmarshal(dataBytes, &data) 64 | return 65 | }, 66 | Marshal: func(data *jaegerConfiguration) ([]byte, error) { 67 | return yaml.Marshal(data) 68 | }, 69 | }, 70 | } 71 | jaegerConfigRoot.Init(lazyLogger) 72 | utils.AddBuildAndStartFuncWatcher(func(f func()) { jaegerConfigRoot.DataHook.AfterMutate = f }) 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/common/configs/license_test.go: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import ( 4 | "fireboom-server/pkg/common/consts" 5 | "fireboom-server/pkg/common/utils" 6 | "fmt" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestGenerateCommunityCode(t *testing.T) { 12 | generateUserLicense(consts.LicenseTypeCommunity, 12, map[string]int{ 13 | consts.LicenseOperation: 888, 14 | consts.LicenseDatasource: 18, 15 | }) 16 | } 17 | 18 | func TestGenerateEnterpriseCode(t *testing.T) { 19 | generateUserLicense(consts.LicenseTypeEnterprise, 12, map[string]int{ 20 | consts.LicenseOperation: -1, 21 | consts.LicenseDatasource: -1, 22 | consts.LicenseImport: 1, 23 | consts.LicenseTeamwork: 1, 24 | consts.LicensePrismaDatasource: 1, 25 | consts.LicenseIncrementBuild: 1, 26 | }) 27 | } 28 | 29 | func generateUserLicense(licenseType consts.LicenseType, months int, userLimits map[string]int) { 30 | requiredUserCode := "1ab53c25d9622a455422cb37cf58e858" 31 | encodeCode := utils.GenerateLicenseKey(requiredUserCode, &userLicense{ 32 | Type: licenseType, 33 | ExpireTime: time.Now().AddDate(0, months, 0), 34 | UserLimits: userLimits, 35 | }) 36 | fmt.Println(encodeCode) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/common/configs/logger.go: -------------------------------------------------------------------------------- 1 | // Package configs 2 | /* 3 | 通过添加writeSyncer,日志会输出到控制台、websocket及日志文件 4 | 控制台输出的日志会保留颜色,其他输出会去除 5 | 不同的启动模式,日志有不同的默认配置 6 | 通过levelEnabler函数实现日志级别的运行时切换 7 | 替换全局日志记录器,使得可以在全局引用同时去依赖 8 | */ 9 | package configs 10 | 11 | import ( 12 | "fireboom-server/pkg/common/consts" 13 | "fireboom-server/pkg/common/utils" 14 | "github.com/wundergraph/wundergraph/pkg/logging" 15 | "go.uber.org/zap" 16 | "go.uber.org/zap/zapcore" 17 | "os" 18 | "regexp" 19 | "sync" 20 | "time" 21 | ) 22 | 23 | var ( 24 | loggerLevel zapcore.Level 25 | loggerWriteSyncers []zapcore.WriteSyncer 26 | colorRegexp = regexp.MustCompile(`\x1b\[[^m]+m([^ ]+)\x1b\[0m`) 27 | lazyLogger func() *zap.Logger 28 | ) 29 | 30 | func removeColorText(p []byte) []byte { 31 | return []byte(colorRegexp.ReplaceAllString(string(p), "$1")) 32 | } 33 | 34 | func addLoggerWriteSyncer(writer zapcore.WriteSyncer) { 35 | loggerWriteSyncers = append(loggerWriteSyncers, writer) 36 | } 37 | 38 | func init() { 39 | lazyLoggerMutex := sync.Mutex{} 40 | lazyLoggerMutex.Lock() 41 | lazyLogger = func() *zap.Logger { 42 | lazyLoggerMutex.Lock() 43 | defer lazyLoggerMutex.Unlock() 44 | return zap.L() 45 | } 46 | utils.RegisterInitMethod(13, func() { 47 | var zapOptions []zap.Option 48 | var defaultEncoderConfig zapcore.EncoderConfig 49 | if utils.GetBoolWithLockViper(consts.DevMode) { 50 | defaultEncoderConfig = zap.NewDevelopmentEncoderConfig() 51 | loggerLevel = zapcore.DebugLevel 52 | host, _ := os.Hostname() 53 | zapOptions = append(zapOptions, 54 | zap.AddCaller(), 55 | zap.AddStacktrace(zap.ErrorLevel), 56 | zap.Fields(zap.String("hostname", host), zap.Int("pid", os.Getpid()))) 57 | } else { 58 | defaultEncoderConfig = zap.NewProductionEncoderConfig() 59 | } 60 | if utils.GetBoolWithLockViper(consts.EnableWebConsole) { 61 | zapOptions = append(zapOptions, zap.WrapCore(func(core zapcore.Core) zapcore.Core { return registerHooks(core, analysis) })) 62 | } 63 | 64 | defaultEncoderConfig.ConsoleSeparator = " " 65 | defaultEncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 66 | defaultEncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.RFC3339Nano) 67 | 68 | loggerWriteSyncers = append(loggerWriteSyncers, zapcore.AddSync(os.Stdout)) 69 | var levelEnablerFunc zap.LevelEnablerFunc 70 | levelEnablerFunc = func(level zapcore.Level) bool { 71 | return loggerLevel.Enabled(level) 72 | } 73 | // 创建一个新的核心,将日志输出到控制台和 WebSocket 74 | consoleCore := zapcore.NewCore( 75 | zapcore.NewConsoleEncoder(defaultEncoderConfig), 76 | zapcore.NewMultiWriteSyncer(loggerWriteSyncers...), 77 | levelEnablerFunc, 78 | ) 79 | 80 | // 替换全局的日志记录器 81 | zap.ReplaceGlobals(zap.New(consoleCore, zapOptions...)) 82 | lazyLoggerMutex.Unlock() 83 | }) 84 | } 85 | 86 | func setLoggerLevel(level string) { 87 | parseLevel, err := zapcore.ParseLevel(level) 88 | if err != nil { 89 | return 90 | } 91 | 92 | loggerLevel = parseLevel 93 | logging.SetLogLevel(loggerLevel) 94 | } 95 | -------------------------------------------------------------------------------- /pkg/common/configs/logger_collector.go: -------------------------------------------------------------------------------- 1 | // Package configs 2 | /* 3 | 添加日志自定义处理的支持 4 | 通过AddCollector函数,设置匹配等级、标识字段、处理函数实现自定义日志处理 5 | 处理函数需要按照约定格式返回,并最终统一发送到websocket 6 | 通过添加自定义处理函数,对日志的中间处理,零依赖实现的问题、运行状态等信息收集 7 | */ 8 | package configs 9 | 10 | import ( 11 | "fireboom-server/pkg/common/utils" 12 | "go.uber.org/zap/zapcore" 13 | "golang.org/x/exp/slices" 14 | ) 15 | 16 | var ( 17 | logCollectors = &utils.SyncMap[*LogCollector, bool]{} 18 | AddFileLoaderQuestionCollector func(string, func(string) map[string]any) 19 | ) 20 | 21 | type LogCollector struct { 22 | MatchLevel []zapcore.Level // 匹配日志等级 23 | IdentifyField string // 关键词字段 24 | Handle func(zapcore.Entry, *zapcore.Field, map[string]*zapcore.Field) *WsMsgBody 25 | } 26 | 27 | func AddCollector(c *LogCollector) { 28 | logCollectors.Store(c, true) 29 | } 30 | 31 | func analysis(entry zapcore.Entry, fields []zapcore.Field) error { 32 | if len(fields) == 0 { 33 | return nil 34 | } 35 | 36 | fieldMap := make(map[string]*zapcore.Field) 37 | for _, field := range fields { 38 | itemField := field 39 | fieldMap[field.Key] = &itemField 40 | } 41 | 42 | handlerCollectors(entry, logCollectors, fieldMap) 43 | return nil 44 | } 45 | 46 | func handlerCollectors(entry zapcore.Entry, collectors *utils.SyncMap[*LogCollector, bool], fieldMap map[string]*zapcore.Field) { 47 | collectors.Range(func(collector *LogCollector, _ bool) bool { 48 | if !slices.Contains(collector.MatchLevel, entry.Level) { 49 | return true 50 | } 51 | 52 | value, ok := fieldMap[collector.IdentifyField] 53 | if !ok { 54 | return true 55 | } 56 | 57 | delete(fieldMap, value.Key) 58 | result := collector.Handle(entry, value, fieldMap) 59 | WebsocketInstance.WriteWsMsgBodyForAll(result) 60 | return true 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /pkg/common/configs/logger_file.go: -------------------------------------------------------------------------------- 1 | // Package configs 2 | /* 3 | 通过lumberjack.Logger和globalSetting来提供对日志配置的支持 4 | 收到pull事件会搜索引擎准备时间(dev为编译开始,start为start开始)后的日志发送到websocket 5 | */ 6 | package configs 7 | 8 | import ( 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/common/utils" 11 | "fireboom-server/pkg/plugins/fileloader" 12 | "gopkg.in/natefinch/lumberjack.v2" 13 | "time" 14 | ) 15 | 16 | const logChannel WsChannel = "log" 17 | 18 | type lumberjackLogger lumberjack.Logger 19 | 20 | func (l *lumberjackLogger) Write(p []byte) (n int, err error) { 21 | return (*lumberjack.Logger)(l).Write(removeColorText(p)) 22 | } 23 | 24 | func (l *lumberjackLogger) Sync() error { 25 | return nil 26 | } 27 | 28 | func (l *lumberjackLogger) initConsoleLogger() { 29 | consoleLoggerText := &fileloader.ModelText[any]{ 30 | Root: utils.StringDot, 31 | ExtensionIgnored: true, 32 | TextRW: &fileloader.SingleTextRW[any]{Name: l.Filename}, 33 | } 34 | consoleLoggerText.Init() 35 | 36 | WsMsgHandlerMap[logChannel] = func(msg *WsMsgBody) any { 37 | switch msg.Event { 38 | case PullEvent: 39 | prepareTime := utils.GetTimeWithLockViper(consts.EnginePrepareTime).Format(time.RFC3339Nano) 40 | content, _ := consoleLoggerText.ReadWithSearch(consoleLoggerText.Title, prepareTime) 41 | return content 42 | } 43 | return nil 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/common/configs/logger_hooked.go: -------------------------------------------------------------------------------- 1 | // Package configs 2 | /* 3 | 重写日志中间件的hooked,辅助实现logger_collector的功能 4 | */ 5 | package configs 6 | 7 | import ( 8 | "go.uber.org/multierr" 9 | "go.uber.org/zap/zapcore" 10 | ) 11 | 12 | type hooked struct { 13 | zapcore.Core 14 | funcs []func(zapcore.Entry, []zapcore.Field) error 15 | } 16 | 17 | func registerHooks(core zapcore.Core, hooks ...func(zapcore.Entry, []zapcore.Field) error) zapcore.Core { 18 | return &hooked{Core: core, funcs: hooks} 19 | } 20 | 21 | func (h *hooked) Level() zapcore.Level { 22 | return zapcore.LevelOf(h.Core) 23 | } 24 | 25 | func (h *hooked) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { 26 | if downstream := h.Core.Check(ent, ce); downstream != nil { 27 | return downstream.AddCore(ent, h) 28 | } 29 | return ce 30 | } 31 | 32 | func (h *hooked) With(fields []zapcore.Field) zapcore.Core { 33 | return &hooked{ 34 | Core: h.Core.With(fields), 35 | funcs: h.funcs, 36 | } 37 | } 38 | 39 | func (h *hooked) Write(ent zapcore.Entry, fields []zapcore.Field) error { 40 | var err error 41 | for i := range h.funcs { 42 | err = multierr.Append(err, h.funcs[i](ent, fields)) 43 | } 44 | return err 45 | } 46 | -------------------------------------------------------------------------------- /pkg/common/configs/resource.go: -------------------------------------------------------------------------------- 1 | // Package configs 2 | /* 3 | 通过fileloader.ModelText和fileloader.Model来管理资源配置 4 | applicationRoot主要是为了验证对properties文件的支持,添加代码级别的配置支持,包括跟路径、认证路由、日志白名单等 5 | BannerText读取内置的文本,即控制台直接输出的飞布LOGO 6 | IntrospectText读取内置的文本,用作graphql数据源的内省查询的请求体 7 | */ 8 | package configs 9 | 10 | import ( 11 | "fireboom-server/pkg/common/consts" 12 | "fireboom-server/pkg/common/utils" 13 | "fireboom-server/pkg/plugins/embed" 14 | "fireboom-server/pkg/plugins/fileloader" 15 | "github.com/magiconair/properties" 16 | "strings" 17 | ) 18 | 19 | type ( 20 | applicationProperties struct { 21 | ContextPath string `properties:"context.path"` 22 | RequestLoggerSkippers string `properties:"request.logger.skippers"` 23 | AuthenticationUrls string `properties:"authentication.urls"` 24 | EngineForwardRequests string `properties:"engine.forward.requests"` 25 | ContactAddress string `properties:"contact.address"` 26 | } 27 | application struct { 28 | ContextPath string 29 | RequestLoggerSkippers []string 30 | AuthenticationUrls []string 31 | EngineForwardRequests []string 32 | ContactAddress string 33 | } 34 | ) 35 | 36 | var ( 37 | BannerText *fileloader.ModelText[any] 38 | IntrospectText *fileloader.ModelText[any] 39 | ApplicationData *application 40 | ) 41 | 42 | func init() { 43 | BannerText = &fileloader.ModelText[any]{ 44 | Root: embed.ResourceRoot, 45 | Extension: fileloader.ExtTxt, 46 | TextRW: &fileloader.EmbedTextRW{EmbedFiles: &embed.ResourceFs, Name: consts.ResourceBanner}, 47 | } 48 | 49 | IntrospectText = &fileloader.ModelText[any]{ 50 | Root: embed.ResourceRoot, 51 | Extension: fileloader.ExtJson, 52 | TextRW: &fileloader.EmbedTextRW{EmbedFiles: &embed.ResourceFs, Name: consts.ResourceIntrospect}, 53 | } 54 | 55 | applicationRoot := &fileloader.Model[applicationProperties]{ 56 | Root: embed.ResourceRoot, 57 | Extension: fileloader.ExtProperties, 58 | DataRW: &fileloader.EmbedDataRW[applicationProperties]{ 59 | EmbedFiles: &embed.ResourceFs, 60 | DataName: consts.ResourceApplication, 61 | Unmarshal: func(dataBytes []byte) (data *applicationProperties, err error) { 62 | props, err := properties.Load(dataBytes, properties.UTF8) 63 | if err != nil { 64 | return 65 | } 66 | 67 | data = &applicationProperties{} 68 | err = props.Decode(data) 69 | return 70 | }, 71 | }, 72 | } 73 | 74 | utils.RegisterInitMethod(12, func() { 75 | BannerText.Init() 76 | IntrospectText.Init() 77 | applicationRoot.Init() 78 | 79 | data := applicationRoot.FirstData() 80 | ApplicationData = &application{ 81 | ContextPath: data.ContextPath, 82 | RequestLoggerSkippers: strings.Split(data.RequestLoggerSkippers, utils.StringComma), 83 | AuthenticationUrls: strings.Split(data.AuthenticationUrls, utils.StringComma), 84 | EngineForwardRequests: strings.Split(data.EngineForwardRequests, utils.StringComma), 85 | ContactAddress: data.ContactAddress, 86 | } 87 | }) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/common/configs/websocket.go: -------------------------------------------------------------------------------- 1 | // Package configs 2 | /* 3 | websocket实现日志记录器writeSyncer接口 4 | applicationRoot主要是为了验证对properties文件的支持,添加代码级别的配置支持,包括跟路径、认证路由、日志白名单等 5 | BannerText读取内置的文本,即控制台直接输出的飞布LOGO 6 | IntrospectText读取内置的文本,用作graphql数据源的内省查询的请求体 7 | */ 8 | package configs 9 | 10 | import ( 11 | "fireboom-server/pkg/common/utils" 12 | "github.com/gorilla/websocket" 13 | json "github.com/json-iterator/go" 14 | "sync" 15 | ) 16 | 17 | type ( 18 | WsMsgBody struct { 19 | Channel WsChannel `json:"channel"` 20 | Event WsEvent `json:"event"` 21 | Data interface{} `json:"data,omitempty"` 22 | Error interface{} `json:"error,omitempty"` 23 | RequestID string `json:"requestId,omitempty"` 24 | } 25 | 26 | WsChannel string 27 | WsEvent string 28 | ) 29 | 30 | type ( 31 | Websocket struct { 32 | Conns utils.SyncMap[*WebsocketConn, bool] 33 | } 34 | WebsocketConn struct { 35 | Conn *websocket.Conn 36 | *sync.Mutex 37 | } 38 | ) 39 | 40 | func (ws *Websocket) Sync() error { 41 | return nil 42 | } 43 | 44 | func (ws *Websocket) Write(p []byte) (n int, err error) { 45 | n = len(p) 46 | ws.WriteWsMsgBodyForAll(&WsMsgBody{ 47 | Channel: logChannel, 48 | Event: PushEvent, 49 | Data: string(removeColorText(p)), 50 | }) 51 | return 52 | } 53 | 54 | func (ws *Websocket) WriteWsMsgBodyForAll(body *WsMsgBody) { 55 | var bodyBytes []byte 56 | ws.Conns.Range(func(item *WebsocketConn, _ bool) bool { 57 | if bodyBytes == nil { 58 | bodyBytes, _ = json.Marshal(body) 59 | } 60 | item.WriteMessage(bodyBytes) 61 | return true 62 | }) 63 | } 64 | 65 | func (ws *WebsocketConn) WriteMessage(bodyBytes []byte) { 66 | ws.Lock() 67 | defer ws.Unlock() 68 | _ = ws.Conn.WriteMessage(websocket.TextMessage, bodyBytes) 69 | } 70 | 71 | func (ws *WebsocketConn) WriteWsMsgBody(body *WsMsgBody) { 72 | bodyBytes, _ := json.Marshal(body) 73 | ws.WriteMessage(bodyBytes) 74 | } 75 | 76 | const ( 77 | PushEvent WsEvent = "push" 78 | PullEvent WsEvent = "pull" 79 | ) 80 | 81 | var ( 82 | WebsocketInstance *Websocket 83 | WsMsgHandlerMap = make(map[WsChannel]func(body *WsMsgBody) any) 84 | ) 85 | 86 | func init() { 87 | utils.RegisterInitMethod(12, func() { 88 | WebsocketInstance = &Websocket{} 89 | addLoggerWriteSyncer(WebsocketInstance) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/common/consts/config_path.go: -------------------------------------------------------------------------------- 1 | // Package consts 2 | /* 3 | 文件路径/文件名常量 4 | */ 5 | package consts 6 | 7 | // 工作目录下的根目录 8 | const ( 9 | RootExported = "exported" 10 | RootStore = "store" 11 | RootUpload = "upload" 12 | RootTemplate = "template" 13 | ) 14 | 15 | // exported目录下的子目录和文件 16 | const ( 17 | ExportedGeneratedParent = "generated" 18 | ExportedIntrospectionParent = "introspection" 19 | ExportedMigrationParent = "migration" 20 | 21 | ExportedGeneratedSwaggerFilename = "swagger" 22 | ExportedGeneratedHookSwaggerFilename = "hook.swagger" 23 | ExportedGeneratedFireboomConfigFilename = "fireboom.config" 24 | ExportedGeneratedFireboomOperationsFilename = "fireboom.operations" 25 | ExportedGeneratedGraphqlSchemaFilename = "fireboom.app.schema" 26 | ) 27 | 28 | // store目录下的子目录 29 | const ( 30 | StoreDatasourceParent = "datasource" 31 | StoreAuthenticationParent = "authentication" 32 | StoreOperationParent = "operation" 33 | StoreStorageParent = "storage" 34 | StoreSdkParent = "sdk" 35 | StoreConfigParent = "config" 36 | StoreRoleParent = "role" 37 | StoreFragmentParent = "fragment" 38 | ) 39 | 40 | // upload目录下的子目录 41 | const ( 42 | UploadOasParent = "oas" 43 | UploadAsyncapiParent = "asyncapi" 44 | UploadPrismaParent = "prisma" 45 | UploadSqliteParent = "sqlite" 46 | UploadGraphqlParent = "graphql" 47 | ) 48 | 49 | // 服务端钩子工作目录下的子目录 50 | const ( 51 | HookGeneratedParent = "generated" 52 | HookGlobalParent = "global" 53 | HookAuthenticationParent = "authentication" 54 | HookOperationParent = "operation" 55 | HookStorageProfileParent = "upload" 56 | HookCustomizeParent = "customize" 57 | HookProxyParent = "proxy" 58 | HookFunctionParent = "function" 59 | HookFragmentsParent = "fragment" 60 | ) 61 | 62 | // 内嵌的文件名/当前工作目录的文件名 63 | const ( 64 | GlobalOperation = "global.operation" 65 | GlobalSetting = "global.setting" 66 | 67 | JaegerConfig = "jaeger.config" 68 | 69 | DefaultEnv = ".env" 70 | 71 | ResourceIntrospect = "introspect" 72 | ResourceApplication = "application" 73 | ResourceBanner = "banner" 74 | 75 | KeyAuthentication = "authentication" 76 | KeyLicense = "license" 77 | ) 78 | -------------------------------------------------------------------------------- /pkg/common/consts/engine_hook.go: -------------------------------------------------------------------------------- 1 | // Package consts 2 | /* 3 | 引擎钩子常量 4 | */ 5 | package consts 6 | 7 | type MiddlewareHook string 8 | 9 | // operation/authentication/global钩子 10 | const ( 11 | PreResolve MiddlewareHook = "preResolve" 12 | MutatingPreResolve MiddlewareHook = "mutatingPreResolve" 13 | MockResolve MiddlewareHook = "mockResolve" 14 | CustomResolve MiddlewareHook = "customResolve" 15 | PostResolve MiddlewareHook = "postResolve" 16 | MutatingPostResolve MiddlewareHook = "mutatingPostResolve" 17 | 18 | PostAuthentication MiddlewareHook = "postAuthentication" 19 | MutatingPostAuthentication MiddlewareHook = "mutatingPostAuthentication" 20 | RevalidateAuthentication MiddlewareHook = "revalidateAuthentication" 21 | PostLogout MiddlewareHook = "postLogout" 22 | 23 | HttpTransportBeforeRequest MiddlewareHook = "beforeOriginRequest" 24 | HttpTransportAfterResponse MiddlewareHook = "afterOriginResponse" 25 | HttpTransportOnRequest MiddlewareHook = "onOriginRequest" 26 | HttpTransportOnResponse MiddlewareHook = "onOriginResponse" 27 | 28 | WsTransportOnConnectionInit MiddlewareHook = "onConnectionInit" 29 | ) 30 | 31 | type UploadHook string 32 | 33 | // upload钩子 34 | const ( 35 | PreUpload UploadHook = "preUpload" 36 | PostUpload UploadHook = "postUpload" 37 | ) 38 | -------------------------------------------------------------------------------- /pkg/common/consts/env_param.go: -------------------------------------------------------------------------------- 1 | // Package consts 2 | /* 3 | 环境变量常量 4 | */ 5 | package consts 6 | 7 | // command params 8 | const ( 9 | WebPort = "web-port" 10 | ActiveMode = "active" 11 | 12 | DevMode = "dev" 13 | EnableAuth = "enable-auth" 14 | EnableRebuild = "enable-rebuild" 15 | EnableSwagger = "enable-swagger" 16 | EnableHookReport = "enable-hook-report" 17 | EnableWebConsole = "enable-web-console" 18 | EnableDebugPprof = "enable-debug-pprof" 19 | EnableLogicDelete = "enable-logic-delete" 20 | RegenerateKey = "regenerate-key" 21 | IgnoreMergeEnvironment = "ignore-merge-environment" 22 | ) 23 | 24 | // command params default value 25 | const ( 26 | DefaultWebPort = "9123" 27 | DefaultProdActive = "prod" 28 | ) 29 | 30 | // env file param 31 | const ( 32 | GithubRawProxyUrl = "GITHUB_RAW_PROXY_URL" 33 | GithubProxyUrl = "GITHUB_PROXY_URL" 34 | FbRepoUrlMirror = "FB_REPO_URL_MIRROR" 35 | FbRawUrlMirror = "FB_RAW_URL_MIRROR" 36 | ) 37 | 38 | // runtime temp param 39 | const ( 40 | FbVersion = "FbVersion" 41 | FbCommit = "FbCommit" 42 | ) 43 | 44 | // engine status param name 45 | const ( 46 | GlobalStartTime = "globalStartTime" 47 | EngineStartTime = "engineStartTime" 48 | EnginePrepareTime = "enginePrepareTime" 49 | EngineStatusField = "engineStatus" 50 | EngineFirstStatus = "engineFirstStatus" 51 | ) 52 | 53 | // engine status param value 54 | const ( 55 | EngineBuilding = "building" 56 | EngineIncrementBuild = "incrementBuild" 57 | EngineBuildSucceed = "buildSucceed" 58 | EngineBuildFailed = "buildFailed" 59 | EngineStarting = "starting" 60 | EngineIncrementStart = "incrementStart" 61 | EngineStartSucceed = "startSucceed" 62 | EngineStartFailed = "startFailed" 63 | ) 64 | 65 | const ( 66 | HookReportTime = "hookReportTime" 67 | HookReportStatus = "hookReportStatus" 68 | ) 69 | 70 | const ( 71 | DatabaseExecuteTimeout = "FB_DATABASE_EXECUTE_TIMEOUT" 72 | DatabaseCloseTimeout = "FB_DATABASE_CLOSE_TIMEOUT" 73 | ) 74 | -------------------------------------------------------------------------------- /pkg/common/consts/graphql_key.go: -------------------------------------------------------------------------------- 1 | // Package consts 2 | /* 3 | graphql常量 4 | */ 5 | package consts 6 | 7 | // graphql role指令 8 | const ( 9 | RequireMatchAll = "requireMatchAll" 10 | RequireMatchAny = "requireMatchAny" 11 | DenyMatchAll = "denyMatchAll" 12 | DenyMatchAny = "denyMatchAny" 13 | ) 14 | 15 | // graphql 根字段所属类型 16 | const ( 17 | TypeQuery = "Query" 18 | TypeMutation = "Mutation" 19 | TypeSubscription = "Subscription" 20 | ) 21 | 22 | // graphql 普通类型定义 23 | const ( 24 | ScalarBoolean = "Boolean" 25 | ScalarInt = "Int" 26 | ScalarFloat = "Float" 27 | ScalarString = "String" 28 | ScalarID = "ID" 29 | ScalarJSON = "JSON" 30 | ) 31 | 32 | // graphql 复杂类型定义(通过stringFormat标识) 33 | const ( 34 | ScalarDate = "Date" 35 | ScalarDateTime = "DateTime" 36 | ScalarBytes = "Bytes" 37 | ScalarBinary = "Binary" 38 | ScalarUUID = "UUID" 39 | ScalarBigInt = "BigInt" 40 | ScalarDecimal = "Decimal" 41 | ScalarGeometry = "Geometry" 42 | ) 43 | -------------------------------------------------------------------------------- /pkg/common/consts/license.go: -------------------------------------------------------------------------------- 1 | // Package consts 2 | /* 3 | license常量 4 | */ 5 | package consts 6 | 7 | const LicenseStatusField = "licenseStatus" 8 | 9 | // license status 10 | const ( 11 | LicenseStatusEmpty = "empty" 12 | LicenseStatusInvalid = "invalid" 13 | LicenseStatusExpired = "expired" 14 | LicenseStatusLimited = "limited" 15 | ) 16 | 17 | type LicenseType string 18 | 19 | // license type 20 | const ( 21 | LicenseTypeCommunity LicenseType = "community" 22 | LicenseTypeProfessional LicenseType = "professional" 23 | LicenseTypeEnterprise LicenseType = "enterprise" 24 | ) 25 | 26 | // license module 27 | const ( 28 | LicenseOperation = "operation" 29 | LicenseDatasource = "datasource" 30 | LicenseImport = "import" 31 | LicenseTeamwork = "teamwork" 32 | LicensePrismaDatasource = "prismaDatasource" 33 | LicenseIncrementBuild = EngineIncrementBuild 34 | ) 35 | -------------------------------------------------------------------------------- /pkg/common/consts/request_param.go: -------------------------------------------------------------------------------- 1 | // Package consts 2 | /* 3 | 请求参数常量 4 | */ 5 | package consts 6 | 7 | const ( 8 | PathParamDataName = "dataName" 9 | 10 | QueryParamDataNames = "dataNames" 11 | QueryParamWatchAction = "watchAction" 12 | QueryParamFilename = "filename" 13 | QueryParamDirname = "dirname" 14 | QueryParamUrl = "url" 15 | QueryParamAuthentication = "auth-key" 16 | QueryParamCrud = "crud" 17 | QueryParamOverwrite = "overwrite" 18 | QueryParamVersion = "version" 19 | 20 | FormParamFile = "file" 21 | 22 | HeaderParamAuthentication = "X-FB-Authentication" 23 | HeaderParamLocale = "X-FB-Locale" 24 | HeaderParamTag = "X-FB-Tag" 25 | HeaderParamUser = "X-FB-User" 26 | 27 | AttachmentFilenameFormat = `attachment;filename="%s"` 28 | ) 29 | -------------------------------------------------------------------------------- /pkg/common/models/datasource_customize.go: -------------------------------------------------------------------------------- 1 | // Package models 2 | /* 3 | 使用fileloader.ModelText管理钩子生成的自定义数据源内省文本 4 | 读取钩子工作目录/customize下的文件 5 | 依赖与父model Datasource实现多文件管理 6 | 当钩子配置变更时,自动切换父目录和重置路径字典 7 | */ 8 | package models 9 | 10 | import ( 11 | "fireboom-server/pkg/common/consts" 12 | "fireboom-server/pkg/common/utils" 13 | "fireboom-server/pkg/plugins/fileloader" 14 | ) 15 | 16 | var DatasourceCustomize *fileloader.ModelText[Datasource] 17 | 18 | func init() { 19 | DatasourceCustomize = &fileloader.ModelText[Datasource]{ 20 | Title: consts.HookCustomizeParent, 21 | Extension: fileloader.ExtJson, 22 | TextRW: &fileloader.MultipleTextRW[Datasource]{ 23 | Enabled: func(item *Datasource, _ ...string) bool { return item.Enabled }, 24 | Name: fileloader.DefaultBasenameFunc(), 25 | }, 26 | } 27 | 28 | utils.RegisterInitMethod(20, func() { 29 | DatasourceCustomize.RelyModel = DatasourceRoot 30 | DatasourceCustomize.Init() 31 | }) 32 | 33 | AddSwitchServerSdkWatcher(func(outputPath, _, _ string, upperFirstBasename bool) { 34 | if outputPath != "" { 35 | outputPath = utils.NormalizePath(outputPath, consts.HookCustomizeParent) 36 | } 37 | DatasourceCustomize.Root = outputPath 38 | DatasourceCustomize.UpperFirstBasename = upperFirstBasename 39 | DatasourceCustomize.ResetRootDirectory() 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/common/models/global_operation.go: -------------------------------------------------------------------------------- 1 | // Package models 2 | /* 3 | 使用fileloader.Model管理GlobalOperation配置 4 | 读取store/config/global_setting.json文件,变更后会触发引擎编译 5 | 全局的operation配置,用来作为operation的部分配置的默认值 6 | */ 7 | package models 8 | 9 | import ( 10 | "fireboom-server/pkg/common/consts" 11 | "fireboom-server/pkg/common/utils" 12 | "fireboom-server/pkg/plugins/embed" 13 | "fireboom-server/pkg/plugins/fileloader" 14 | "github.com/wundergraph/wundergraph/pkg/wgpb" 15 | ) 16 | 17 | type GlobalOperation struct { 18 | CacheConfig *wgpb.OperationCacheConfig `json:"cacheConfig"` 19 | LiveQueryConfig *wgpb.OperationLiveQueryConfig `json:"liveQueryConfig"` 20 | GraphqlTransformEnabled bool `json:"graphqlTransformEnabled"` 21 | AuthenticationConfigs map[wgpb.OperationType]*wgpb.OperationAuthenticationConfig `json:"authenticationConfigs"` 22 | ApiAuthenticationHooks map[consts.MiddlewareHook]bool `json:"apiAuthenticationHooks"` 23 | GlobalHttpTransportHooks map[consts.MiddlewareHook]bool `json:"globalHttpTransportHooks"` 24 | } 25 | 26 | var ( 27 | globalOperationDefaultText *fileloader.ModelText[GlobalOperation] 28 | GlobalOperationRoot *fileloader.Model[GlobalOperation] 29 | ) 30 | 31 | func init() { 32 | globalOperationDefaultText = &fileloader.ModelText[GlobalOperation]{ 33 | Root: embed.DefaultRoot, 34 | Extension: fileloader.ExtJson, 35 | TextRW: &fileloader.EmbedTextRW{EmbedFiles: &embed.DefaultFs, Name: consts.GlobalOperation}, 36 | } 37 | 38 | utils.RegisterInitMethod(20, func() { 39 | globalOperationDefaultText.Init() 40 | 41 | GlobalOperationRoot = &fileloader.Model[GlobalOperation]{ 42 | Root: utils.NormalizePath(consts.RootStore, consts.StoreConfigParent), 43 | Extension: fileloader.ExtJson, 44 | DataHook: &fileloader.DataHook[GlobalOperation]{}, 45 | DataRW: &fileloader.SingleDataRW[GlobalOperation]{ 46 | InitDataBytes: globalOperationDefaultText.GetFirstCache(), 47 | DataName: consts.GlobalOperation, 48 | }, 49 | } 50 | GlobalOperationRoot.Init() 51 | utils.AddBuildAndStartFuncWatcher(func(f func()) { GlobalOperationRoot.DataHook.AfterMutate = f }) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/common/models/hook.go: -------------------------------------------------------------------------------- 1 | // Package models 2 | /* 3 | 提供快捷构建钩子代码文件路径的管理 4 | 初始化时设置pathFunc和enabledFunc并在运行时返回真实的结果 5 | */ 6 | package models 7 | 8 | import ( 9 | "fireboom-server/pkg/common/configs" 10 | "fireboom-server/pkg/common/consts" 11 | "fireboom-server/pkg/common/utils" 12 | "fireboom-server/pkg/plugins/fileloader" 13 | ) 14 | 15 | type ( 16 | HookOptions map[consts.MiddlewareHook]*hookOption 17 | hookOption struct { 18 | Path string `json:"path"` 19 | Existed bool `json:"existed"` 20 | Enabled bool `json:"enabled"` 21 | pathFunc func(string, ...string) string 22 | enabledFunc func(string, ...string) bool 23 | } 24 | ) 25 | 26 | func buildHookOption[T any](modelText *fileloader.ModelText[T]) *hookOption { 27 | return &hookOption{pathFunc: modelText.GetPath, enabledFunc: modelText.Enabled} 28 | } 29 | 30 | func getHookOptionResultMap(optionMap HookOptions, dataName string, optional ...string) (result HookOptions) { 31 | result = make(HookOptions) 32 | for hook, option := range optionMap { 33 | result[hook] = getHookOptionResultItem(option, dataName, optional...) 34 | } 35 | return 36 | } 37 | 38 | func getHookOptionResultItem(option *hookOption, dataName string, optional ...string) *hookOption { 39 | hookPath := option.pathFunc(dataName, optional...) 40 | return &hookOption{ 41 | Path: hookPath, 42 | Existed: !utils.NotExistFile(hookPath), 43 | Enabled: option.enabledFunc(dataName, optional...), 44 | } 45 | } 46 | 47 | func GetHookServerUrl() string { 48 | return utils.GetVariableString(configs.GlobalSettingRoot.FirstData().ServerOptions.ServerUrl) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/common/models/operation_extension.go: -------------------------------------------------------------------------------- 1 | // Package models 2 | /* 3 | 使用fileloader.ModelText管理function, proxy接口配置 4 | 当钩子配置变更时,自动切换父目录和重置路径字典 5 | */ 6 | package models 7 | 8 | import ( 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/common/utils" 11 | "fireboom-server/pkg/plugins/fileloader" 12 | "strings" 13 | ) 14 | 15 | var ( 16 | OperationFunction *fileloader.ModelText[Operation] 17 | OperationProxy *fileloader.ModelText[Operation] 18 | ) 19 | 20 | func buildOperationExtension(parent string) *fileloader.ModelText[Operation] { 21 | item := &fileloader.ModelText[Operation]{ 22 | Title: parent, 23 | Extension: fileloader.ExtJson, 24 | TextRW: &fileloader.MultipleTextRW[Operation]{ 25 | Enabled: func(item *Operation, _ ...string) bool { return item.Enabled }, 26 | Name: func(dataName string, _ int, _ ...string) (string, bool) { 27 | return strings.TrimPrefix(dataName, parent+"/"), true 28 | }, 29 | }, 30 | } 31 | utils.RegisterInitMethod(20, func() { 32 | item.RelyModel = OperationRoot 33 | item.Init() 34 | }) 35 | AddSwitchServerSdkWatcher(func(outputPath, _, _ string, upperFirstBasename bool) { 36 | if outputPath != "" { 37 | outputPath = utils.NormalizePath(outputPath, parent) 38 | } 39 | item.Root = outputPath 40 | item.UpperFirstBasename = upperFirstBasename 41 | item.ResetRootDirectory() 42 | }) 43 | return item 44 | } 45 | 46 | func init() { 47 | OperationFunction = buildOperationExtension(consts.HookFunctionParent) 48 | OperationProxy = buildOperationExtension(consts.HookProxyParent) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/common/models/operation_graphql.go: -------------------------------------------------------------------------------- 1 | // Package models 2 | /* 3 | 使用fileloader.ModelText管理graphql文本 4 | 依赖与父model graphql实现多文件管理 5 | */ 6 | package models 7 | 8 | import ( 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/common/utils" 11 | "fireboom-server/pkg/plugins/fileloader" 12 | ) 13 | 14 | var OperationGraphql *fileloader.ModelText[Operation] 15 | 16 | func init() { 17 | OperationGraphql = &fileloader.ModelText[Operation]{ 18 | Title: "operation.graphql", 19 | Root: utils.NormalizePath(consts.RootStore, consts.StoreOperationParent), 20 | Extension: fileloader.ExtGraphql, 21 | ReadCacheRequired: true, 22 | TextRW: &fileloader.MultipleTextRW[Operation]{ 23 | Enabled: func(item *Operation, _ ...string) bool { return item.Enabled }, 24 | Name: fileloader.DefaultBasenameFunc(), 25 | }, 26 | } 27 | 28 | utils.RegisterInitMethod(20, func() { 29 | OperationGraphql.RelyModel = OperationRoot 30 | OperationGraphql.Init() 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/common/models/operation_graphql_history.go: -------------------------------------------------------------------------------- 1 | // Package models 2 | /* 3 | 使用fileloader.ModelText管理graphql文本 4 | 依赖与父model graphql实现多文件管理 5 | */ 6 | package models 7 | 8 | import ( 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/common/utils" 11 | "fireboom-server/pkg/plugins/fileloader" 12 | "strings" 13 | ) 14 | 15 | var OperationGraphqlHistory *fileloader.ModelText[Operation] 16 | 17 | func init() { 18 | OperationGraphqlHistory = &fileloader.ModelText[Operation]{ 19 | Title: "operation.graphql.history", 20 | Root: utils.NormalizePath(consts.RootStore, consts.StoreOperationParent), 21 | Extension: fileloader.ExtGraphql, 22 | SkipRelyModelUpdate: true, 23 | TextRW: &fileloader.MultipleTextRW[Operation]{ 24 | Enabled: func(item *Operation, _ ...string) bool { return true }, 25 | Name: func(dataName string, _ int, elem ...string) (string, bool) { 26 | path := make([]string, len(elem)+1) 27 | if lastSplitIndex := strings.LastIndexByte(dataName, '/'); lastSplitIndex != -1 { 28 | dataName = dataName[:lastSplitIndex+1] + "." + dataName[lastSplitIndex+1:] 29 | } else { 30 | dataName = "." + dataName 31 | } 32 | path[0] = dataName 33 | copy(path[1:], elem) 34 | return utils.NormalizePath(path...), true 35 | }, 36 | }, 37 | } 38 | 39 | utils.RegisterInitMethod(20, func() { 40 | OperationGraphqlHistory.RelyModel = OperationRoot 41 | OperationGraphqlHistory.Init() 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/common/models/plugin_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fireboom-server/pkg/common/utils" 5 | "fireboom-server/pkg/plugins/fileloader" 6 | "fmt" 7 | "github.com/wundergraph/wundergraph/pkg/wgpb" 8 | "testing" 9 | ) 10 | 11 | func TestPluginTree_DeleteByIds(t *testing.T) { 12 | m := &fileloader.Model[Operation]{ 13 | Root: "../../../store/operation", 14 | DataRW: &fileloader.MultipleDataRW[Operation]{ 15 | GetDataName: func(item *Operation) string { return item.Path }, 16 | SetDataName: func(item *Operation, name string) { item.Path = name }, 17 | Filter: func(item *Operation) bool { return item.DeleteTime == "" }, 18 | LogicDelete: func(item *Operation) { item.DeleteTime = utils.TimeFormatNow() }, 19 | }, 20 | } 21 | m.Init() 22 | dataList := m.ListByCondition(func(item *Operation) bool { 23 | return item.OperationType == wgpb.OperationType_MUTATION 24 | }) 25 | path := "CDKEY/RedeemCDKEY" 26 | insertBytes := []byte(fmt.Sprintf("{\"path\": \"%s\", \"remark\": \"小宝你好呀\", \"cacheConfig\": {\"enabled\": true, \"maxAge\": 111}}", path)) 27 | updateBytes := []byte(fmt.Sprintf("{\"path\": \"%s\", \"enabled\": true, \"cacheConfig\": null}", path)) 28 | insertData, insertErr := m.Insert(insertBytes, "fireboom") 29 | lockErr := m.TryLockData(path, "fireboom") 30 | updateData1, updateErr1 := m.UpdateByDataName(updateBytes, "fireboom1") 31 | getWithLock1, getWithLockErr1 := m.GetWithLockUserByDataName(path) 32 | updateData2, updateErr2 := m.UpdateByDataName(updateBytes, "fireboom") 33 | getWithLock2, getWithLockErr2 := m.GetWithLockUserByDataName(path) 34 | dataTrees, err := m.GetDataTrees() 35 | fmt.Println(dataList, insertData, insertErr, lockErr, updateData1, updateErr1, updateData2, updateErr2, 36 | getWithLock1, getWithLock2, getWithLockErr1, getWithLockErr2, dataTrees, err) 37 | 38 | } 39 | -------------------------------------------------------------------------------- /pkg/common/models/role.go: -------------------------------------------------------------------------------- 1 | // Package models 2 | /* 3 | 使用fileloader.Model管理角色配置 4 | 读取store/role下的文件,支持逻辑删除,变更后会触发引擎编译 5 | */ 6 | package models 7 | 8 | import ( 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/common/utils" 11 | "fireboom-server/pkg/plugins/fileloader" 12 | ) 13 | 14 | type Role struct { 15 | Code string `json:"code"` 16 | Remark string `json:"remark"` 17 | CreateTime string `json:"createTime"` 18 | UpdateTime string `json:"updateTime"` 19 | DeleteTime string `json:"deleteTime"` 20 | } 21 | 22 | var RoleRoot *fileloader.Model[Role] 23 | 24 | func init() { 25 | RoleRoot = &fileloader.Model[Role]{ 26 | Root: utils.NormalizePath(consts.RootStore, consts.StoreRoleParent), 27 | Extension: fileloader.ExtJson, 28 | DataHook: &fileloader.DataHook[Role]{ 29 | OnInsert: func(item *Role) error { 30 | item.CreateTime = utils.TimeFormatNow() 31 | return nil 32 | }, 33 | OnUpdate: func(_, dst *Role, user string) error { 34 | if user != fileloader.SystemUser { 35 | dst.UpdateTime = utils.TimeFormatNow() 36 | } 37 | return nil 38 | }, 39 | }, 40 | DataRW: &fileloader.MultipleDataRW[Role]{ 41 | GetDataName: func(item *Role) string { return item.Code }, 42 | SetDataName: func(item *Role, name string) { item.Code = name }, 43 | Filter: func(item *Role) bool { return item.DeleteTime == "" }, 44 | LogicDelete: func(item *Role) { item.DeleteTime = utils.TimeFormatNow() }, 45 | }, 46 | } 47 | 48 | utils.RegisterInitMethod(20, func() { 49 | RoleRoot.Init() 50 | utils.AddBuildAndStartFuncWatcher(func(f func()) { RoleRoot.DataHook.AfterMutate = f }) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/common/models/sdk_extension.go: -------------------------------------------------------------------------------- 1 | // Package models 2 | /* 3 | 使用fileloader.ModelText管理钩子的模板/输出目录 4 | */ 5 | package models 6 | 7 | import ( 8 | "fireboom-server/pkg/common/consts" 9 | "fireboom-server/pkg/common/utils" 10 | "fireboom-server/pkg/plugins/fileloader" 11 | ) 12 | 13 | func init() { 14 | sdkOutput := &fileloader.ModelText[Sdk]{ 15 | Title: "sdk.output", 16 | ExtensionIgnored: true, 17 | RelyModelActionIgnored: true, 18 | TextRW: &fileloader.MultipleTextRW[Sdk]{ 19 | Enabled: func(item *Sdk, _ ...string) bool { return item.Enabled }, 20 | Name: func(dataName string, _ int, _ ...string) (path string, extension bool) { 21 | data, err := SdkRoot.GetByDataName(dataName) 22 | if err != nil { 23 | return 24 | } 25 | 26 | path = data.OutputPath 27 | return 28 | }, 29 | }, 30 | } 31 | 32 | sdkTemplate := &fileloader.ModelText[Sdk]{ 33 | Title: "sdk.template", 34 | Root: consts.RootTemplate, 35 | ExtensionIgnored: true, 36 | TextRW: &fileloader.MultipleTextRW[Sdk]{ 37 | Enabled: func(item *Sdk, _ ...string) bool { return item.Enabled }, 38 | Name: func(dataName string, _ int, _ ...string) (string, bool) { 39 | return dataName, false 40 | }, 41 | }, 42 | } 43 | 44 | utils.RegisterInitMethod(20, func() { 45 | sdkOutput.RelyModel = SdkRoot 46 | sdkTemplate.RelyModel = SdkRoot 47 | sdkOutput.Init() 48 | sdkTemplate.Init() 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/common/models/storage.go: -------------------------------------------------------------------------------- 1 | // Package models 2 | /* 3 | 使用fileloader.Model管理存储配置 4 | 读取store/storage下的文件,支持逻辑删除,变更后会触发引擎编译 5 | 每个storage可以添加多个profile来实现上传的验证和自定义逻辑处理 6 | */ 7 | package models 8 | 9 | import ( 10 | "fireboom-server/pkg/common/configs" 11 | "fireboom-server/pkg/common/consts" 12 | "fireboom-server/pkg/common/utils" 13 | "fireboom-server/pkg/plugins/fileloader" 14 | "github.com/wundergraph/wundergraph/pkg/wgpb" 15 | ) 16 | 17 | type Storage struct { 18 | Enabled bool `json:"enabled"` 19 | CreateTime string `json:"createTime"` 20 | UpdateTime string `json:"updateTime"` 21 | DeleteTime string `json:"deleteTime"` 22 | 23 | wgpb.S3UploadConfiguration 24 | } 25 | 26 | var StorageRoot *fileloader.Model[Storage] 27 | 28 | func init() { 29 | StorageRoot = &fileloader.Model[Storage]{ 30 | Root: utils.NormalizePath(consts.RootStore, consts.StoreStorageParent), 31 | Extension: fileloader.ExtJson, 32 | DataHook: &fileloader.DataHook[Storage]{ 33 | OnInsert: func(item *Storage) error { 34 | item.CreateTime = utils.TimeFormatNow() 35 | return nil 36 | }, 37 | OnUpdate: func(_, dst *Storage, user string) error { 38 | if user != fileloader.SystemUser { 39 | dst.UpdateTime = utils.TimeFormatNow() 40 | } 41 | return nil 42 | }, 43 | AfterInsert: func(item *Storage, _ string) bool { return item.Enabled }, 44 | AfterRename: func(src, _ *Storage, _ string) { 45 | ClientCache.removeClient(src.Name) 46 | }, 47 | AfterDelete: func(dataName string, _ string) { 48 | ClientCache.removeClient(dataName) 49 | }, 50 | }, 51 | DataRW: &fileloader.MultipleDataRW[Storage]{ 52 | GetDataName: func(item *Storage) string { return item.Name }, 53 | SetDataName: func(item *Storage, name string) { item.Name = name }, 54 | Filter: func(item *Storage) bool { return item.DeleteTime == "" }, 55 | LogicDelete: func(item *Storage) { item.DeleteTime = utils.TimeFormatNow() }, 56 | }, 57 | } 58 | 59 | utils.RegisterInitMethod(20, func() { 60 | StorageRoot.Init() 61 | configs.AddFileLoaderQuestionCollector(StorageRoot.GetModelName(), func(dataName string) map[string]any { 62 | data, _ := StorageRoot.GetByDataName(dataName) 63 | if data == nil { 64 | return nil 65 | } 66 | 67 | return map[string]any{fieldEnabled: data.Enabled} 68 | }) 69 | utils.AddBuildAndStartFuncWatcher(func(f func()) { StorageRoot.DataHook.AfterMutate = f }) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/common/models/storage_profile_hook.go: -------------------------------------------------------------------------------- 1 | // Package models 2 | /* 3 | 使用fileloader.ModelText管理钩子工作目录下代码文件 4 | 读取钩子工作目录/storage下的代码文件 5 | 当钩子配置变更时,自动切换父目录和重置路径字典 6 | 与其他model不同的是,钩子配置允许多个且在一个文件中,通过WatcherPath监听uploadProfiles属性变更并实现自定义操作 7 | 得益于fileloader中增量变更的实现,可以精确定位到实际变更的属性及其变更规则 8 | */ 9 | package models 10 | 11 | import ( 12 | "fireboom-server/pkg/common/consts" 13 | "fireboom-server/pkg/common/utils" 14 | "fireboom-server/pkg/plugins/fileloader" 15 | "github.com/wundergraph/wundergraph/pkg/wgpb" 16 | ) 17 | 18 | var StorageProfileHookOptionMap HookOptions 19 | 20 | func GetStorageProfileHookOptions(dataName, profile string) HookOptions { 21 | return getHookOptionResultMap(StorageProfileHookOptionMap, dataName, profile) 22 | } 23 | 24 | func buildS3UploadProfileHook(hook consts.UploadHook, enabledFunc func(item *wgpb.S3UploadProfileHooksConfiguration) bool) { 25 | item := &fileloader.ModelText[Storage]{ 26 | Title: string(hook), 27 | RelyModelWatchPath: []string{"uploadProfiles"}, 28 | TextRW: &fileloader.MultipleTextRW[Storage]{ 29 | Enabled: func(item *Storage, optional ...string) bool { 30 | if !item.Enabled || len(optional) == 0 { 31 | return false 32 | } 33 | 34 | profile, ok := item.UploadProfiles[optional[0]] 35 | if !ok || profile.Hooks == nil { 36 | return false 37 | } 38 | 39 | return enabledFunc(profile.Hooks) 40 | }, 41 | Name: func(dataName string, offset int, optional ...string) (basename string, ext bool) { 42 | if len(optional) < offset+1 { 43 | basename = dataName 44 | return 45 | } 46 | 47 | ext = true 48 | basename = utils.NormalizePath(dataName, optional[offset], string(hook)) 49 | return 50 | }, 51 | }, 52 | } 53 | utils.RegisterInitMethod(20, func() { 54 | item.RelyModel = StorageRoot 55 | item.Init() 56 | }) 57 | AddSwitchServerSdkWatcher(func(outputPath, codePackage, extension string, upperFirstBasename bool) { 58 | item.Extension = fileloader.Extension(extension) 59 | if outputPath != "" { 60 | if codePackage != "" { 61 | outputPath = utils.NormalizePath(outputPath, codePackage) 62 | } 63 | outputPath = utils.NormalizePath(outputPath, consts.HookStorageProfileParent) 64 | } 65 | item.Root = outputPath 66 | item.UpperFirstBasename = upperFirstBasename 67 | item.ResetRootDirectory() 68 | }) 69 | StorageProfileHookOptionMap[consts.MiddlewareHook(hook)] = buildHookOption(item) 70 | } 71 | 72 | func init() { 73 | StorageProfileHookOptionMap = make(HookOptions) 74 | 75 | buildS3UploadProfileHook(consts.PreUpload, func(item *wgpb.S3UploadProfileHooksConfiguration) bool { 76 | return item.PreUpload 77 | }) 78 | buildS3UploadProfileHook(consts.PostUpload, func(item *wgpb.S3UploadProfileHooksConfiguration) bool { 79 | return item.PostUpload 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/common/utils/date.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "time" 4 | 5 | // TimeFormatNow 当前时间格式化字符串 6 | func TimeFormatNow() string { 7 | return TimeFormat(time.Now()) 8 | } 9 | 10 | // TimeFormat 按照2006-01-02T15:04:05.999999999Z07:00格式转换输入时间 11 | func TimeFormat(t time.Time) string { 12 | return t.Format(time.RFC3339Nano) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/common/utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | // HttpPost 发送post请求,支持超时设置 12 | func HttpPost(url string, reqBody []byte, headers map[string]string, timeout ...int) (respBody []byte, err error) { 13 | r, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(reqBody)) 14 | if err != nil { 15 | return 16 | } 17 | 18 | for k, v := range headers { 19 | r.Header.Add(k, v) 20 | } 21 | 22 | client := http.DefaultClient 23 | if len(timeout) > 0 { 24 | client.Timeout = time.Duration(timeout[0]) * time.Second 25 | } 26 | 27 | resp, err := client.Do(r) 28 | if err != nil { 29 | return 30 | } 31 | 32 | respBody, err = io.ReadAll(resp.Body) 33 | if err != nil { 34 | return 35 | } 36 | 37 | if resp.StatusCode != http.StatusOK { 38 | err = errors.New(string(respBody)) 39 | respBody = nil 40 | return 41 | } 42 | 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /pkg/common/utils/init_method.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fireboom-server/pkg/plugins/i18n" 5 | "golang.org/x/exp/slices" 6 | ) 7 | 8 | var initMethods SyncMap[*initMethod, bool] 9 | 10 | type initMethod struct { 11 | order int 12 | method func() 13 | caller string 14 | } 15 | 16 | // RegisterInitMethod 注册初始化函数,使得原本不可控的init函数得以按顺序执行 17 | // 编排系统启动时的初始化函数 18 | func RegisterInitMethod(order int, method func()) { 19 | initMethods.Store(&initMethod{ 20 | order: order, 21 | method: method, 22 | caller: i18n.GetCallerMode(), 23 | }, true) 24 | } 25 | 26 | // ExecuteInitMethods 执行初始化函数,order优先,再按照caller排序 27 | func ExecuteInitMethods() { 28 | inits := initMethods.Keys() 29 | slices.SortFunc(inits, func(a, b *initMethod) bool { 30 | return a.order < b.order || a.order == b.order && a.caller < b.caller 31 | }) 32 | 33 | for _, item := range inits { 34 | item.method() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/common/utils/license.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/md5" 8 | "encoding/base64" 9 | "encoding/hex" 10 | json "github.com/json-iterator/go" 11 | "github.com/shirou/gopsutil/v3/host" 12 | "strings" 13 | ) 14 | 15 | // GenerateUserCode 通过mac地址,主机信息生成用户唯一不可逆标识码 16 | func GenerateUserCode() string { 17 | hash := md5.Sum([]byte(GetHostInfoString())) 18 | return hex.EncodeToString(hash[:]) 19 | } 20 | 21 | // GenerateLicenseKey 使用加密工具和用户标识码并添加额外信息生成license 22 | func GenerateLicenseKey(userCode string, data any) string { 23 | licenseBytes, err := json.Marshal(data) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | iv := make([]byte, cipherAead.NonceSize()) 29 | copy(iv[:], userCode) 30 | cipherBytes := cipherAead.Seal(nil, iv, licenseBytes, []byte(`😄`)) 31 | iv = append(iv, cipherBytes...) 32 | return base64.StdEncoding.EncodeToString(iv) 33 | } 34 | 35 | // DecodeLicenseKey 解密license,并与用户唯一标识码比对 36 | func DecodeLicenseKey(encodedCode string) []byte { 37 | nonceSize := cipherAead.NonceSize() 38 | decodeBytes, err := base64.StdEncoding.DecodeString(encodedCode) 39 | if err != nil || len(decodeBytes) < nonceSize { 40 | return nil 41 | } 42 | 43 | iv := decodeBytes[:nonceSize] 44 | cipherBytes := decodeBytes[nonceSize:] 45 | licenseBytes, err := cipherAead.Open(nil, iv, cipherBytes, []byte(`😄`)) 46 | if err != nil { 47 | return nil 48 | } 49 | 50 | ivExpected := make([]byte, cipherAead.NonceSize()) 51 | copy(ivExpected[:], GenerateUserCode()) 52 | if !bytes.Equal(iv, ivExpected) { 53 | return nil 54 | } 55 | 56 | return licenseBytes 57 | } 58 | 59 | // GetHostInfoString 获取主机信息并将其中变化参数设置为常量 60 | // 通过修改hostId实现对之前版本的兼容 61 | func GetHostInfoString() string { 62 | hostInfo, err := host.Info() 63 | if err != nil { 64 | panic(err) 65 | } 66 | hostInfo.BootTime = 20230308 67 | hostInfo.Uptime = 20230520 68 | hostInfo.Procs = 1314 69 | return strings.ReplaceAll(hostInfo.String(), `"hostId":`, `"hostid":`) 70 | } 71 | 72 | var cipherAead cipher.AEAD 73 | 74 | func init() { 75 | keyBytes := make([]byte, 32) 76 | copy(keyBytes[:], "😄202305201314😄") 77 | block, err := aes.NewCipher(keyBytes) 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | cipherAead, err = cipher.NewGCM(block) 83 | if err != nil { 84 | panic(err) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pkg/common/utils/path.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | ) 7 | 8 | const ArrayPath = "[]" 9 | 10 | // NormalizePath 格式化路径成linux系统路径 11 | func NormalizePath(elem ...string) string { 12 | return filepath.ToSlash(filepath.Join(elem...)) 13 | } 14 | 15 | // AppendIfMissSlash 追加路径分隔符(仅当缺失情况) 16 | func AppendIfMissSlash(path string) string { 17 | if strings.HasSuffix(path, "/") { 18 | return path 19 | } 20 | 21 | return path + "/" 22 | } 23 | 24 | // CopyAndAppendItem 拷贝并追加字符,并判断是否添加'[]' 25 | func CopyAndAppendItem(path []string, item string, appendArrayFunc ...func() bool) []string { 26 | pathLen := len(path) 27 | pathCap := pathLen + 1 28 | var appendRequired bool 29 | if len(appendArrayFunc) > 0 && appendArrayFunc[0]() { 30 | appendRequired = true 31 | pathCap++ 32 | } 33 | itemPath := make([]string, pathLen, pathCap) 34 | copy(itemPath, path) 35 | itemPath = append(itemPath, item) 36 | if appendRequired { 37 | itemPath = append(itemPath, ArrayPath) 38 | } 39 | return itemPath 40 | } 41 | -------------------------------------------------------------------------------- /pkg/common/utils/swagger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/getkin/kin-openapi/openapi3" 5 | "github.com/wundergraph/wundergraph/pkg/wgpb" 6 | "net/http" 7 | "strconv" 8 | ) 9 | 10 | var ( 11 | StatusOkText = http.StatusText(http.StatusOK) 12 | StatusBadRequestText = http.StatusText(http.StatusBadRequest) 13 | ) 14 | 15 | // MakeApiOperationRequestBody 构建OperationType_MUTATION的入参定义 16 | func MakeApiOperationRequestBody(requestSchemaRef *openapi3.SchemaRef, multipartForms ...*wgpb.OperationMultipartForm) *openapi3.RequestBodyRef { 17 | var content openapi3.Content 18 | if len(multipartForms) > 0 { 19 | content = openapi3.NewContentWithFormDataSchemaRef(requestSchemaRef) 20 | } else { 21 | content = openapi3.NewContentWithJSONSchemaRef(requestSchemaRef) 22 | } 23 | return &openapi3.RequestBodyRef{ 24 | Value: &openapi3.RequestBody{Content: content}, 25 | } 26 | } 27 | 28 | // MakeApiOperationResponse 构建返回参数的定义 29 | func MakeApiOperationResponse(responseSchema *openapi3.SchemaRef) openapi3.Responses { 30 | return openapi3.Responses{ 31 | strconv.Itoa(http.StatusOK): { 32 | Value: &openapi3.Response{ 33 | Description: &StatusOkText, 34 | Content: openapi3.NewContentWithJSONSchemaRef(responseSchema), 35 | }, 36 | }, 37 | strconv.Itoa(http.StatusBadRequest): { 38 | Value: &openapi3.Response{ 39 | Description: &StatusBadRequestText, 40 | }, 41 | }, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/common/utils/sync_map.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | json "github.com/json-iterator/go" 5 | "sync" 6 | ) 7 | 8 | type SyncMap[K comparable, V any] struct { 9 | sync.Map 10 | } 11 | 12 | func (s *SyncMap[K, V]) MarshalJSON() ([]byte, error) { 13 | if s == nil { 14 | return nil, nil 15 | } 16 | 17 | return json.Marshal(s.ToMap()) 18 | } 19 | 20 | func (s *SyncMap[K, V]) UnmarshalJSON(data []byte) error { 21 | dataMap := make(map[K]V) 22 | if err := json.Unmarshal(data, &dataMap); err != nil { 23 | return err 24 | } 25 | 26 | *s = SyncMap[K, V]{} 27 | for k, v := range dataMap { 28 | s.Map.Store(k, v) 29 | } 30 | return nil 31 | } 32 | 33 | func (s *SyncMap[K, V]) Load(key K) (v V, ok bool) { 34 | data, ok := s.Map.Load(key) 35 | if !ok { 36 | return 37 | } 38 | 39 | v, ok = data.(V) 40 | return 41 | } 42 | 43 | func (s *SyncMap[K, V]) Contains(key K) (ok bool) { 44 | _, ok = s.Load(key) 45 | return 46 | } 47 | 48 | func (s *SyncMap[K, V]) LoadOrStore(key K, value V) (v V, ok bool) { 49 | data, existed := s.Map.LoadOrStore(key, value) 50 | v, ok = data.(V) 51 | ok = ok && existed 52 | return 53 | } 54 | 55 | func (s *SyncMap[K, V]) Store(key K, value V) { 56 | s.Map.Store(key, value) 57 | } 58 | 59 | func (s *SyncMap[K, V]) Range(f func(key K, value V) bool) { 60 | if s == nil { 61 | return 62 | } 63 | s.Map.Range(func(key, value any) bool { 64 | keyStr, keyOk := key.(K) 65 | valObj, valOk := value.(V) 66 | if !keyOk || !valOk { 67 | return false 68 | } 69 | return f(keyStr, valObj) 70 | }) 71 | } 72 | 73 | func (s *SyncMap[K, V]) ToMap() (dataMap map[K]V) { 74 | dataMap = make(map[K]V) 75 | s.Range(func(key K, value V) bool { 76 | dataMap[key] = value 77 | return true 78 | }) 79 | return 80 | } 81 | 82 | func (s *SyncMap[K, V]) Keys() (keys []K) { 83 | keys = make([]K, 0) 84 | s.Range(func(key K, _ V) bool { 85 | keys = append(keys, key) 86 | return true 87 | }) 88 | return 89 | } 90 | 91 | func (s *SyncMap[K, V]) Size() (count int) { 92 | s.Range(func(key K, _ V) bool { 93 | count++ 94 | return true 95 | }) 96 | return 97 | } 98 | 99 | func (s *SyncMap[K, V]) FirstValue() (v V) { 100 | s.Range(func(_ K, value V) bool { 101 | v = value 102 | return false 103 | }) 104 | return 105 | } 106 | 107 | func (s *SyncMap[K, V]) IsEmpty() (b bool) { 108 | b = true 109 | s.Range(func(_ K, value V) bool { 110 | b = false 111 | return false 112 | }) 113 | return 114 | } 115 | 116 | func (s *SyncMap[K, V]) Clear() (keys []K) { 117 | s.Range(func(key K, _ V) bool { 118 | s.Map.Delete(key) 119 | return true 120 | }) 121 | return 122 | } 123 | -------------------------------------------------------------------------------- /pkg/common/utils/validate_value.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "golang.org/x/exp/slices" 5 | "reflect" 6 | ) 7 | 8 | var validateLengthKinds = []reflect.Kind{reflect.Map, reflect.Slice} 9 | 10 | // IsZeroValue 判断值是否为零值 11 | func IsZeroValue(val any) bool { 12 | if nil == val { 13 | return true 14 | } 15 | if _, ok := val.(reflect.Kind); ok { 16 | return true 17 | } 18 | value := reflect.ValueOf(val) 19 | if value.IsZero() { 20 | return true 21 | } 22 | 23 | if slices.Contains(validateLengthKinds, value.Kind()) { 24 | return value.Len() == 0 25 | } 26 | 27 | return false 28 | } 29 | -------------------------------------------------------------------------------- /pkg/common/utils/viper_lock.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | "os" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | var viperMutex = &sync.Mutex{} 12 | 13 | // SetWithLockViper 带锁设置viper的值 14 | func SetWithLockViper(key string, value interface{}, skipIfExisted ...bool) { 15 | viperMutex.Lock() 16 | defer viperMutex.Unlock() 17 | 18 | if len(skipIfExisted) > 0 && skipIfExisted[0] && viper.IsSet(key) { 19 | return 20 | } 21 | viper.Set(key, value) 22 | _ = os.Setenv(key, fmt.Sprintf("%v", value)) 23 | } 24 | 25 | // GetBoolWithLockViper 带锁获取viper中的bool值 26 | func GetBoolWithLockViper(key string) bool { 27 | viperMutex.Lock() 28 | defer viperMutex.Unlock() 29 | 30 | return viper.GetBool(key) 31 | } 32 | 33 | // GetStringWithLockViper 带锁获取viper中的string值 34 | func GetStringWithLockViper(key string) string { 35 | viperMutex.Lock() 36 | defer viperMutex.Unlock() 37 | 38 | return viper.GetString(key) 39 | } 40 | 41 | // GetTimeWithLockViper 带锁获取viper中的time值 42 | func GetTimeWithLockViper(key string) time.Time { 43 | viperMutex.Lock() 44 | defer viperMutex.Unlock() 45 | 46 | return viper.GetTime(key) 47 | } 48 | 49 | // GetInt32WithLockViper 带锁获取viper中的int32值 50 | func GetInt32WithLockViper(key string) int32 { 51 | viperMutex.Lock() 52 | defer viperMutex.Unlock() 53 | 54 | return viper.GetInt32(key) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/engine/asyncapi/binding.go: -------------------------------------------------------------------------------- 1 | package asyncapi 2 | 3 | type BindingKey string 4 | 5 | const ( 6 | BindingKey_http BindingKey = "http" 7 | BindingKey_ws BindingKey = "ws" 8 | BindingKey_kafka BindingKey = "kafka" 9 | BindingKey_anypointmq BindingKey = "anypointmq" 10 | BindingKey_amqp BindingKey = "amqp" 11 | BindingKey_mqtt BindingKey = "mqtt" 12 | BindingKey_mqtt5 BindingKey = "mqtt5" 13 | BindingKey_nats BindingKey = "nats" 14 | BindingKey_jms BindingKey = "jms" 15 | BindingKey_sns BindingKey = "sns" 16 | BindingKey_solace BindingKey = "solace" 17 | BindingKey_sqs BindingKey = "sqs" 18 | BindingKey_stomp BindingKey = "stomp" 19 | BindingKey_redis BindingKey = "redis" 20 | BindingKey_mercure BindingKey = "mercure" 21 | BindingKey_ibmmq BindingKey = "ibmmq" 22 | BindingKey_googlepubsub BindingKey = "googlepubsub" 23 | BindingKey_pulsar BindingKey = "pulsar" 24 | ) 25 | -------------------------------------------------------------------------------- /pkg/engine/asyncapi/operation_trait.go: -------------------------------------------------------------------------------- 1 | package asyncapi 2 | 3 | import ( 4 | "github.com/getkin/kin-openapi/openapi3" 5 | "github.com/go-openapi/jsonpointer" 6 | json "github.com/json-iterator/go" 7 | ) 8 | 9 | type ( 10 | OperationTraits []*OperationTraitRef 11 | OperationTraitRef struct { 12 | Ref string 13 | Value *OperationTrait 14 | } 15 | OperationTrait struct { 16 | // A human-friendly title for the operation. 17 | Title string `json:"title" yaml:"title"` 18 | // A short summary of what the operation is about. 19 | Summary string `json:"summary" yaml:"summary"` 20 | // A verbose explanation of the operation. CommonMark syntax can be used for rich text representation. 21 | Description string `json:"description" yaml:"description"` 22 | // A declaration of which security schemes are associated with this operation. Only one of the security scheme objects MUST be satisfied to authorize an operation. In cases where Server Security also applies, it MUST also be satisfied. 23 | Security *SecuritySchemeRef `json:"security" yaml:"security"` 24 | // A list of tags for logical grouping and categorization of operations. 25 | Tags Tags `json:"tags" yaml:"tags"` 26 | // Additional external documentation for this operation. 27 | ExternalDocs *ExtensionDocsRef `json:"externalDocs" yaml:"externalDocs"` 28 | // A map where the keys describe the name of the protocol and the values describe protocol-specific definitions for the operation. 29 | Bindings OperationBindings `json:"bindings" yaml:"bindings"` 30 | } 31 | ) 32 | 33 | // MarshalYAML returns the YAML encoding of OperationTraitRef. 34 | func (x OperationTraitRef) MarshalYAML() (interface{}, error) { 35 | if ref := x.Ref; ref != "" { 36 | return &openapi3.Ref{Ref: ref}, nil 37 | } 38 | return x.Value, nil 39 | } 40 | 41 | // MarshalJSON returns the JSON encoding of OperationTraitRef. 42 | func (x OperationTraitRef) MarshalJSON() ([]byte, error) { 43 | if ref := x.Ref; ref != "" { 44 | return json.Marshal(openapi3.Ref{Ref: ref}) 45 | } 46 | return json.Marshal(x.Value) 47 | } 48 | 49 | // UnmarshalJSON sets OperationTraitRef to a copy of data. 50 | func (x *OperationTraitRef) UnmarshalJSON(data []byte) error { 51 | var refOnly openapi3.Ref 52 | if err := json.Unmarshal(data, &refOnly); err == nil && refOnly.Ref != "" { 53 | x.Ref = refOnly.Ref 54 | return nil 55 | } 56 | return json.Unmarshal(data, &x.Value) 57 | } 58 | 59 | // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable 60 | func (x *OperationTraitRef) JSONLookup(token string) (interface{}, error) { 61 | if token == "$ref" { 62 | return x.Ref, nil 63 | } 64 | ptr, _, err := jsonpointer.GetForToken(x.Value, token) 65 | return ptr, err 66 | } 67 | -------------------------------------------------------------------------------- /pkg/engine/build/upload_configuration.go: -------------------------------------------------------------------------------- 1 | // Package build 2 | /* 3 | 读取store/storage配置并转换成引擎所需的配置 4 | */ 5 | package build 6 | 7 | import ( 8 | "fireboom-server/pkg/common/models" 9 | "fireboom-server/pkg/common/utils" 10 | json "github.com/json-iterator/go" 11 | "github.com/wundergraph/wundergraph/pkg/wgpb" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | func init() { 16 | utils.RegisterInitMethod(30, func() { 17 | addResolve(4, func() Resolve { return &uploadConfiguration{modelName: models.StorageRoot.GetModelName()} }) 18 | }) 19 | } 20 | 21 | type uploadConfiguration struct { 22 | modelName string 23 | } 24 | 25 | func (u *uploadConfiguration) Resolve(builder *Builder) (err error) { 26 | storages := models.StorageRoot.ListByCondition(func(item *models.Storage) bool { return item.Enabled }) 27 | for _, storage := range storages { 28 | builder.DefinedApi.S3UploadConfiguration = append(builder.DefinedApi.S3UploadConfiguration, u.buildStorageItem(storage)) 29 | } 30 | return 31 | } 32 | 33 | func (u *uploadConfiguration) buildStorageItem(storage *models.Storage) (config *wgpb.S3UploadConfiguration) { 34 | config = &wgpb.S3UploadConfiguration{ 35 | Name: storage.Name, 36 | Endpoint: storage.Endpoint, 37 | AccessKeyID: storage.AccessKeyID, 38 | SecretAccessKey: storage.SecretAccessKey, 39 | BucketName: storage.BucketName, 40 | BucketLocation: storage.BucketLocation, 41 | UseSSL: storage.UseSSL, 42 | UploadProfiles: make(map[string]*wgpb.S3UploadProfile), 43 | } 44 | for profileName, profile := range storage.UploadProfiles { 45 | copyProfile := &wgpb.S3UploadProfile{ 46 | RequireAuthentication: profile.RequireAuthentication, 47 | MaxAllowedUploadSizeBytes: profile.MaxAllowedUploadSizeBytes, 48 | MaxAllowedFiles: profile.MaxAllowedFiles, 49 | AllowedMimeTypes: profile.AllowedMimeTypes, 50 | AllowedFileExtensions: profile.AllowedFileExtensions, 51 | MetadataJSONSchema: profile.MetadataJSONSchema, 52 | } 53 | // 合成上传钩子配置 54 | hookConfigMap := make(map[string]bool) 55 | for hook, option := range models.GetStorageProfileHookOptions(storage.Name, profileName) { 56 | hookConfigMap[string(hook)] = option.Enabled && option.Existed 57 | } 58 | configBytes, _ := json.Marshal(hookConfigMap) 59 | _ = json.Unmarshal(configBytes, ©Profile.Hooks) 60 | config.UploadProfiles[profileName] = copyProfile 61 | } 62 | logger.Debug("build storage succeed", zap.String(u.modelName, storage.Name)) 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /pkg/engine/build/upload_configuration_eventbus.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "fireboom-server/pkg/common/models" 5 | "github.com/wundergraph/wundergraph/pkg/eventbus" 6 | ) 7 | 8 | func (u *uploadConfiguration) Subscribe() { 9 | eventbus.Subscribe(eventbus.ChannelStorage, eventbus.EventInsert, func(data any) any { 10 | return u.buildStorageItem(data.(*models.Storage)) 11 | }) 12 | eventbus.Subscribe(eventbus.ChannelStorage, eventbus.EventUpdate, func(data any) any { 13 | return u.buildStorageItem(data.(*models.Storage)) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/engine/build/webhooks.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import ( 4 | "github.com/wundergraph/wundergraph/pkg/wgpb" 5 | ) 6 | 7 | func init() { 8 | addResolve(5, func() Resolve { return &webhooks{} }) 9 | } 10 | 11 | type webhooks struct{} 12 | 13 | func (w *webhooks) Resolve(builder *Builder) (err error) { 14 | builder.DefinedApi.Webhooks = make([]*wgpb.WebhookConfiguration, 0) 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /pkg/engine/datasource/asyncapi.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "fireboom-server/pkg/common/models" 5 | "fireboom-server/pkg/common/utils" 6 | "fireboom-server/pkg/engine/asyncapi" 7 | "fireboom-server/pkg/plugins/fileloader" 8 | "github.com/ghodss/yaml" 9 | json "github.com/json-iterator/go" 10 | "github.com/vektah/gqlparser/v2/ast" 11 | "github.com/wundergraph/wundergraph/pkg/wgpb" 12 | "golang.org/x/exp/slices" 13 | "path/filepath" 14 | ) 15 | 16 | func init() { 17 | actionMap[wgpb.DataSourceKind_ASYNCAPI] = func(ds *models.Datasource, _ string) Action { 18 | return &actionAsyncapi{ds: ds} 19 | } 20 | } 21 | 22 | type actionAsyncapi struct { 23 | ds *models.Datasource 24 | doc *asyncapi.Spec 25 | } 26 | 27 | func (a *actionAsyncapi) Introspect() (string, error) { 28 | return "", nil 29 | } 30 | 31 | func (a *actionAsyncapi) BuildDataSourceConfiguration(document *ast.SchemaDocument) (*wgpb.DataSourceConfiguration, error) { 32 | return nil, nil 33 | } 34 | 35 | func (a *actionAsyncapi) RuntimeDataSourceConfiguration(configuration *wgpb.DataSourceConfiguration) ([]*wgpb.DataSourceConfiguration, []*wgpb.FieldConfiguration, error) { 36 | return nil, nil, nil 37 | } 38 | 39 | // 处理rest数据源依赖的文本,支持.yaml, .yml, .json等类型文件 40 | // 添加了对openapi2和3的支持 41 | func (a *actionAsyncapi) fetchDocument() (err error) { 42 | oasFilepath := models.DatasourceUploadAsyncapi.GetPath(a.ds.Name) 43 | oasBytes, err := utils.ReadFileAsUTF8(oasFilepath) 44 | if err != nil { 45 | return 46 | } 47 | 48 | ext := fileloader.Extension(filepath.Ext(oasFilepath)) 49 | if slices.Contains(yamlExtensions, ext) { 50 | if oasBytes, err = yaml.YAMLToJSON(oasBytes); err != nil { 51 | return 52 | } 53 | } 54 | 55 | if err = json.Unmarshal(oasBytes, &a.doc); err != nil { 56 | return 57 | } 58 | 59 | if err = a.doc.ResolveRefsIn(); err != nil { 60 | return 61 | } 62 | 63 | return a.doc.Validate() 64 | } 65 | -------------------------------------------------------------------------------- /pkg/engine/datasource/common_cache.go: -------------------------------------------------------------------------------- 1 | // Package datasource 2 | /* 3 | 使用prisma引擎的数据源的缓存,其中graphql缓存适用与其他数据源的缓存 4 | */ 5 | package datasource 6 | 7 | import ( 8 | "fireboom-server/pkg/common/consts" 9 | "fireboom-server/pkg/common/models" 10 | "fireboom-server/pkg/common/utils" 11 | "fireboom-server/pkg/plugins/fileloader" 12 | ) 13 | 14 | const ( 15 | cacheSchema = "schema" 16 | cacheDmmf = "dmmf" 17 | ) 18 | 19 | var ( 20 | CachePrismaSchemaText *fileloader.ModelText[models.Datasource] 21 | CacheGraphqlSchemaText *fileloader.ModelText[models.Datasource] 22 | CacheDmmfText *fileloader.ModelText[models.Datasource] 23 | introspectionDirname = utils.NormalizePath(consts.RootExported, consts.ExportedIntrospectionParent) 24 | migrationDirname = utils.NormalizePath(consts.RootExported, consts.ExportedMigrationParent) 25 | ) 26 | 27 | func buildDatasourceCache(name string, extension fileloader.Extension) *fileloader.ModelText[models.Datasource] { 28 | itemText := &fileloader.ModelText[models.Datasource]{ 29 | Title: name, 30 | Root: introspectionDirname, 31 | Extension: extension, 32 | TextRW: &fileloader.MultipleTextRW[models.Datasource]{ 33 | Enabled: func(item *models.Datasource, _ ...string) bool { return item.Enabled }, 34 | Name: fileloader.DefaultBasenameFunc(name), 35 | }, 36 | } 37 | utils.RegisterInitMethod(30, func() { 38 | itemText.RelyModel = models.DatasourceRoot 39 | itemText.Init() 40 | }) 41 | return itemText 42 | } 43 | 44 | func init() { 45 | CachePrismaSchemaText = buildDatasourceCache(cacheSchema, fileloader.ExtPrisma) 46 | CacheGraphqlSchemaText = buildDatasourceCache(cacheSchema, fileloader.ExtGraphql) 47 | CacheDmmfText = buildDatasourceCache(cacheDmmf, fileloader.ExtJson) 48 | } 49 | -------------------------------------------------------------------------------- /pkg/engine/datasource/prisma_engine_query.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "context" 5 | "fireboom-server/pkg/common/consts" 6 | "github.com/wundergraph/wundergraph/pkg/datasources/database" 7 | "go.uber.org/zap" 8 | "net/http" 9 | "path/filepath" 10 | "time" 11 | ) 12 | 13 | // IntrospectGraphqlSchema 内省graphql schema 14 | func IntrospectGraphqlSchema(engineInput EngineInput) (graphqlSchema string, err error) { 15 | return startQueryEngineWithAction[string](engineInput, func(ctx context.Context, engine *database.Engine) (string, error) { 16 | return engine.IntrospectGraphQLSchema(ctx) 17 | }) 18 | } 19 | 20 | // IntrospectDMMF 内省DMMF 21 | func IntrospectDMMF(engineInput EngineInput) (dmmfContent string, err error) { 22 | return startQueryEngineWithAction[string](engineInput, func(ctx context.Context, engine *database.Engine) (string, error) { 23 | return engine.IntrospectDMMF(ctx) 24 | }) 25 | } 26 | 27 | func startQueryEngineWithAction[O any](engineInput EngineInput, action func(context.Context, *database.Engine) (O, error)) (o O, err error) { 28 | engine := database.NewEngine(&http.Client{Timeout: time.Second * 30}, zap.L(), consts.RootExported) 29 | // 改造成适用绝对路径,解决文本过长出现的命令行too many arguments list报错 30 | prismaSchemaFilepath, _ := filepath.Abs(engineInput.PrismaSchemaFilepath) 31 | if err = engine.StartQueryEngine(prismaSchemaFilepath, buildEnvironments(engineInput)...); err != nil { 32 | return 33 | } 34 | defer engine.StopPrismaEngine() 35 | 36 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) 37 | defer cancel() 38 | if err = engine.WaitUntilReady(ctx); err != nil { 39 | return 40 | } 41 | 42 | return action(ctx, engine) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/engine/datasource/prisma_extend.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "fireboom-server/pkg/common/consts" 5 | "github.com/vektah/gqlparser/v2/ast" 6 | "github.com/wundergraph/wundergraph/pkg/datasources/database" 7 | "github.com/wundergraph/wundergraph/pkg/wgpb" 8 | ) 9 | 10 | const ( 11 | optionalRawFieldQueryRaw = "optional_queryRaw" 12 | optionalRawFieldExecuteRaw = "optional_executeRaw" 13 | ) 14 | 15 | var originRawFields = map[string]string{ 16 | optionalRawFieldQueryRaw: "queryRaw", 17 | optionalRawFieldExecuteRaw: "executeRaw", 18 | } 19 | 20 | func getRawFieldOriginName(kind wgpb.DataSourceKind, fieldName string) string { 21 | if !database.SupportOptionalRaw(kind) { 22 | return fieldName 23 | } 24 | if name, ok := originRawFields[fieldName]; ok { 25 | fieldName = name 26 | } 27 | return fieldName 28 | } 29 | 30 | func extendOptionalRawField(kind wgpb.DataSourceKind, document *ast.SchemaDocument) { 31 | if !database.SupportOptionalRaw(kind) { 32 | return 33 | } 34 | mutations := document.Definitions.ForName(consts.TypeMutation) 35 | if mutations == nil { 36 | return 37 | } 38 | mutations.Fields = append(mutations.Fields, 39 | makeExtendRawField(optionalRawFieldQueryRaw), 40 | makeExtendRawField(optionalRawFieldExecuteRaw)) 41 | } 42 | 43 | func makeExtendRawField(name string) *ast.FieldDefinition { 44 | return &ast.FieldDefinition{ 45 | Name: name, 46 | Arguments: []*ast.ArgumentDefinition{{ 47 | Name: "query", 48 | Type: &ast.Type{NonNull: true, Elem: &ast.Type{NamedType: consts.ScalarString}}, 49 | }}, 50 | Type: &ast.Type{NamedType: consts.ScalarJSON}, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/engine/directives/operation_disallowparallel.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现OperationDirective接口,只能定义在LocationQuery, LocationMutation, LocationSubscription上 4 | Resolve 标识operation禁止并行解析 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/plugins/i18n" 10 | "github.com/vektah/gqlparser/v2/ast" 11 | "github.com/wundergraph/wundergraph/pkg/engineconfigloader" 12 | ) 13 | 14 | const disallowParallelName = "disallowParallel" 15 | 16 | type disallowParallel struct{} 17 | 18 | func (o *disallowParallel) Directive() *ast.DirectiveDefinition { 19 | return &ast.DirectiveDefinition{ 20 | Description: prependMockWorked(appendIfExistExampleGraphql(i18n.DisallowParallelDesc.String())), 21 | Name: disallowParallelName, 22 | Locations: []ast.DirectiveLocation{ast.LocationQuery, ast.LocationMutation}, 23 | } 24 | } 25 | 26 | func (o *disallowParallel) Definitions() ast.DefinitionList { 27 | return nil 28 | } 29 | 30 | func (o *disallowParallel) Resolve(*OperationResolver) error { 31 | return nil 32 | } 33 | 34 | func init() { 35 | registerDirective(disallowParallelName, &disallowParallel{}) 36 | engineconfigloader.AddDisallowParallelFetchDirective(disallowParallelName) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/engine/directives/operation_internaloperation.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现OperationDirective接口,只能定义在LocationQuery, LocationMutation, LocationSubscription上 4 | Resolve 标识operation内部,引擎会仅注册/internal路由 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/plugins/i18n" 10 | "github.com/vektah/gqlparser/v2/ast" 11 | ) 12 | 13 | const internalOperationName = "internalOperation" 14 | 15 | type internalOperation struct{} 16 | 17 | func (o *internalOperation) Directive() *ast.DirectiveDefinition { 18 | return &ast.DirectiveDefinition{ 19 | Description: appendIfExistExampleGraphql(i18n.InternalOperationDesc.String()), 20 | Name: internalOperationName, 21 | Locations: []ast.DirectiveLocation{ast.LocationQuery, ast.LocationMutation, ast.LocationSubscription}, 22 | } 23 | } 24 | 25 | func (o *internalOperation) Definitions() ast.DefinitionList { 26 | return nil 27 | } 28 | 29 | func (o *internalOperation) Resolve(resolver *OperationResolver) error { 30 | resolver.Operation.Internal = true 31 | return nil 32 | } 33 | 34 | func init() { 35 | registerDirective(internalOperationName, &internalOperation{}) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/engine/directives/selection_asyncresolve.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现SelectionDirective接口,只能定义在LocationField上 4 | Resolve 标识参数内部传递,与@export组合使用 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/plugins/i18n" 10 | "fmt" 11 | "github.com/getkin/kin-openapi/openapi3" 12 | "github.com/vektah/gqlparser/v2/ast" 13 | ) 14 | 15 | const asyncResolveName = "asyncResolve" 16 | 17 | type asyncResolve struct{} 18 | 19 | func (s *asyncResolve) Directive() *ast.DirectiveDefinition { 20 | return &ast.DirectiveDefinition{ 21 | Description: prependMockWorked(appendIfExistExampleGraphql(i18n.AsyncResolveDesc.String())), 22 | Name: asyncResolveName, 23 | Locations: []ast.DirectiveLocation{ast.LocationField}, 24 | } 25 | } 26 | 27 | func (s *asyncResolve) Definitions() ast.DefinitionList { 28 | return nil 29 | } 30 | 31 | func (s *asyncResolve) Resolve(resolver *SelectionResolver) (err error) { 32 | if resolver.Schema.Value.Type != openapi3.TypeArray { 33 | err = fmt.Errorf("@%s directive only support on array type", asyncResolveName) 34 | return 35 | } 36 | return 37 | } 38 | 39 | func init() { 40 | registerDirective(asyncResolveName, &asyncResolve{}) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/engine/directives/selection_export.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现SelectionDirective接口,只能定义在LocationField上 4 | Resolve判断导出的参数是否在传递参数中定义 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/common/utils" 11 | "fireboom-server/pkg/plugins/i18n" 12 | "fmt" 13 | "github.com/vektah/gqlparser/v2/ast" 14 | "golang.org/x/exp/slices" 15 | ) 16 | 17 | const ( 18 | exportName = "export" 19 | exportArgName = "as" 20 | exportDirectiveRequiredFormat = `expected variable [%s] with directive @internal for @export(as: "%s") on path [%s]` 21 | ) 22 | 23 | type export struct{} 24 | 25 | func (s *export) Directive() *ast.DirectiveDefinition { 26 | return &ast.DirectiveDefinition{ 27 | Description: prependMockWorked(appendIfExistExampleGraphql(i18n.ExportDesc.String())), 28 | Name: exportName, 29 | Locations: []ast.DirectiveLocation{ast.LocationField}, 30 | Arguments: ast.ArgumentDefinitionList{{ 31 | Name: exportArgName, 32 | Type: ast.NonNullNamedType(consts.ScalarString, nil), 33 | }}, 34 | } 35 | } 36 | 37 | func (s *export) Definitions() ast.DefinitionList { 38 | return nil 39 | } 40 | 41 | func (s *export) Resolve(resolver *SelectionResolver) error { 42 | value, ok := resolver.Arguments[exportArgName] 43 | if !ok { 44 | return fmt.Errorf(argumentRequiredFormat, exportArgName) 45 | } 46 | 47 | if !slices.ContainsFunc(resolver.OperationDefinition.VariableDefinitions, func(variableDefinition *ast.VariableDefinition) bool { 48 | internalDirective := variableDefinition.Directives.ForName(internalName) 49 | return internalDirective != nil && variableDefinition.Variable == value 50 | }) { 51 | return fmt.Errorf(exportDirectiveRequiredFormat, value, value, utils.JoinStringWithDot(resolver.Path...)) 52 | } 53 | resolver.VariableExported[value] = true 54 | return nil 55 | } 56 | 57 | func init() { 58 | registerDirective(exportName, &export{}) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/engine/directives/selection_exportmatch.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现SelectionDirective接口,只能定义在LocationField上 4 | Resolve判断导出的参数是否在传递参数中定义 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/common/utils" 11 | "fireboom-server/pkg/plugins/i18n" 12 | "fmt" 13 | "github.com/vektah/gqlparser/v2/ast" 14 | "golang.org/x/exp/slices" 15 | ) 16 | 17 | const ( 18 | exportMatchName = "exportMatch" 19 | exportMatchArgName = "for" 20 | ) 21 | 22 | type exportMatch struct{} 23 | 24 | func (s *exportMatch) Directive() *ast.DirectiveDefinition { 25 | return &ast.DirectiveDefinition{ 26 | Description: prependMockWorked(appendIfExistExampleGraphql(i18n.ExportMatchDesc.String())), 27 | Name: exportMatchName, 28 | Locations: []ast.DirectiveLocation{ast.LocationField}, 29 | Arguments: ast.ArgumentDefinitionList{{ 30 | Name: exportMatchArgName, 31 | Type: ast.NonNullNamedType(consts.ScalarString, nil), 32 | }}, 33 | } 34 | } 35 | 36 | func (s *exportMatch) Definitions() ast.DefinitionList { 37 | return nil 38 | } 39 | 40 | func (s *exportMatch) Resolve(resolver *SelectionResolver) error { 41 | value, ok := resolver.Arguments[exportMatchArgName] 42 | if !ok { 43 | return fmt.Errorf(argumentRequiredFormat, exportMatchArgName) 44 | } 45 | 46 | if !slices.ContainsFunc(resolver.OperationDefinition.VariableDefinitions, func(variableDefinition *ast.VariableDefinition) bool { 47 | internalDirective := variableDefinition.Directives.ForName(internalName) 48 | return internalDirective != nil && variableDefinition.Variable == value 49 | }) { 50 | return fmt.Errorf(exportDirectiveRequiredFormat, value, value, utils.JoinStringWithDot(resolver.Path...)) 51 | } 52 | return nil 53 | } 54 | 55 | func init() { 56 | registerDirective(exportMatchName, &exportMatch{}) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/engine/directives/selection_firstRawResult.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现SelectionDirective接口,只能定义在LocationField上 4 | Resolve 标识参数内部传递,与@export组合使用 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/plugins/i18n" 10 | "github.com/vektah/gqlparser/v2/ast" 11 | ) 12 | 13 | const firstRawResultName = "firstRawResult" 14 | 15 | type firstRawResult struct{} 16 | 17 | func (s *firstRawResult) Directive() *ast.DirectiveDefinition { 18 | return &ast.DirectiveDefinition{ 19 | Description: prependMockWorked(appendIfExistExampleGraphql(i18n.FirstRawResultDesc.String())), 20 | Name: firstRawResultName, 21 | Locations: []ast.DirectiveLocation{ast.LocationField}, 22 | } 23 | } 24 | 25 | func (s *firstRawResult) Definitions() ast.DefinitionList { 26 | return nil 27 | } 28 | 29 | func (s *firstRawResult) Resolve(resolver *SelectionResolver) (err error) { 30 | return 31 | } 32 | 33 | func FieldQueryRawWrapArrayRequired(directives ast.DirectiveList, fieldOriginName string) bool { 34 | wrapArrayRequired := fieldOriginName == "queryRaw" || fieldOriginName == "optional_queryRaw" 35 | if wrapArrayRequired { 36 | for _, item := range directives { 37 | directive := selectionDirectiveMap[item.Name] 38 | if _, ok := directive.(*firstRawResult); ok { 39 | wrapArrayRequired = false 40 | break 41 | } 42 | } 43 | } 44 | return wrapArrayRequired 45 | } 46 | 47 | func init() { 48 | registerDirective(firstRawResultName, &firstRawResult{}) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/engine/directives/selection_formatdatetime.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现SelectionDirective接口,只能定义在LocationField上 4 | Resolve 按照引擎需要的配置定义PostResolveTransformations,留作后续处理graphql响应时格式化日期格式 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/common/utils" 10 | "fireboom-server/pkg/plugins/i18n" 11 | "fmt" 12 | "github.com/spf13/cast" 13 | "github.com/vektah/gqlparser/v2/ast" 14 | "github.com/wundergraph/wundergraph/pkg/pool" 15 | "golang.org/x/exp/slices" 16 | ) 17 | 18 | const ( 19 | formatDateTimeName = "formatDateTime" 20 | formatDateTimeNotAllowedFormat = "expected format %v, but found [%s:%s]" 21 | ) 22 | 23 | var formatDateTimeAllows = []string{"date", "date-time"} 24 | 25 | type formatDateTime struct{} 26 | 27 | func (s *formatDateTime) Directive() *ast.DirectiveDefinition { 28 | return &ast.DirectiveDefinition{ 29 | Description: prependMockWorked(appendIfExistExampleGraphql(i18n.FormatDateTimeDesc.String())), 30 | Name: formatDateTimeName, 31 | Locations: []ast.DirectiveLocation{ast.LocationField}, 32 | Arguments: dateTimeFormatArguments(), 33 | } 34 | } 35 | 36 | func (s *formatDateTime) Definitions() ast.DefinitionList { 37 | return dateTimeFormatDefinitions() 38 | } 39 | 40 | func (s *formatDateTime) Resolve(resolver *SelectionResolver) (err error) { 41 | if format := resolver.Schema.Value.Format; !slices.Contains(formatDateTimeAllows, format) { 42 | return fmt.Errorf(formatDateTimeNotAllowedFormat, formatDateTimeAllows, resolver.Schema.Value.Type, format) 43 | } 44 | 45 | if len(dateFormatArgValue(resolver.Arguments)) == 0 { 46 | return fmt.Errorf(argumentRequiredFormat, utils.JoinString("/", dateTimeArgFormat, dateTimeArgCustomFormat)) 47 | } 48 | return 49 | } 50 | 51 | func init() { 52 | registerDirective(formatDateTimeName, &formatDateTime{}) 53 | pool.DateFormatFunc = func(args map[string]string, value string) string { 54 | return cast.ToTime(value).Format(dateFormatArgValue(args)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/engine/directives/selection_include.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现SelectionDirective接口,只能定义在LocationField上 4 | Resolve 标识参数内部传递,与@export组合使用 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/common/utils" 11 | "fmt" 12 | "github.com/getkin/kin-openapi/openapi3" 13 | "github.com/vektah/gqlparser/v2/ast" 14 | "strings" 15 | ) 16 | 17 | const ( 18 | includeName = "include" 19 | argIfName = "if" 20 | argIfRuleName = "ifRule" 21 | VariablePrefix = "$" 22 | argumentMustSupplyOneOfFormat = `must supply one of arguments [%s]` 23 | ) 24 | 25 | var includeDirective = &include{} 26 | 27 | type include struct{} 28 | 29 | func (s *include) Directive() *ast.DirectiveDefinition { 30 | return &ast.DirectiveDefinition{ 31 | Description: prependMockWorked(""), 32 | Name: includeName, 33 | Locations: []ast.DirectiveLocation{ast.LocationField, ast.LocationFragmentSpread, ast.LocationInlineFragment}, 34 | Arguments: ast.ArgumentDefinitionList{{ 35 | Name: argIfName, 36 | Type: ast.NamedType(consts.ScalarBoolean, nil), 37 | }, { 38 | Name: argIfRuleName, 39 | Type: ast.NamedType(consts.ScalarString, nil), 40 | }}, 41 | } 42 | } 43 | 44 | func (s *include) Definitions() ast.DefinitionList { 45 | return nil 46 | } 47 | 48 | func (s *include) Resolve(resolver *SelectionResolver) (err error) { 49 | argIfValue, argIfFound := resolver.Arguments[argIfName] 50 | argIfRuleValue, argIfRuleFound := resolver.Arguments[argIfRuleName] 51 | if !argIfFound && !argIfRuleFound { 52 | err = fmt.Errorf(argumentMustSupplyOneOfFormat, utils.JoinString(",", argIfName, argIfRuleName)) 53 | return 54 | } 55 | 56 | resolver.Schema.Value.Nullable = true 57 | if argIfFound { 58 | if err = s.addVariablesSchema(resolver, argIfValue, boolSchema); err != nil { 59 | return 60 | } 61 | } 62 | if argIfRuleFound { 63 | if err = s.addVariablesSchema(resolver, argIfRuleValue, stringSchema); err != nil { 64 | return 65 | } 66 | resolver.Operation.RuleExpressionExisted = true 67 | } 68 | return 69 | } 70 | 71 | var ( 72 | stringSchema = openapi3.NewStringSchema() 73 | boolSchema = openapi3.NewBoolSchema() 74 | ) 75 | 76 | func (s *include) addVariablesSchema(resolver *SelectionResolver, argValue string, argSchema *openapi3.Schema) (err error) { 77 | argValue, ok := strings.CutPrefix(argValue, VariablePrefix) 78 | if !ok { 79 | return 80 | } 81 | if resolver.OperationDefinition.VariableDefinitions.ForName(argValue) == nil { 82 | err = fmt.Errorf("variable [%s] not found", argValue) 83 | return 84 | } 85 | resolver.VariableSchemas[argValue] = &openapi3.SchemaRef{Value: argSchema} 86 | return 87 | } 88 | 89 | func init() { 90 | registerDirective(includeName, &include{}) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/engine/directives/selection_skip.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现SelectionDirective接口,只能定义在LocationField上 4 | Resolve 标识参数内部传递,与@export组合使用 5 | */ 6 | package directives 7 | 8 | import ( 9 | "github.com/vektah/gqlparser/v2/ast" 10 | ) 11 | 12 | const skipName = "skip" 13 | 14 | type skip struct{} 15 | 16 | func (s *skip) Directive() *ast.DirectiveDefinition { 17 | definition := includeDirective.Directive() 18 | definition.Name = skipName 19 | return definition 20 | } 21 | 22 | func (s *skip) Definitions() ast.DefinitionList { 23 | return nil 24 | } 25 | 26 | func (s *skip) Resolve(resolver *SelectionResolver) (err error) { 27 | return includeDirective.Resolve(resolver) 28 | } 29 | 30 | func init() { 31 | registerDirective(skipName, &skip{}) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/engine/directives/selection_skipvariable.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现SelectionDirective接口,只能定义在LocationField上 4 | Resolve 标识参数内部传递,与@export组合使用 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/plugins/i18n" 11 | "github.com/vektah/gqlparser/v2/ast" 12 | ) 13 | 14 | const ( 15 | skipVariableName = "skipVariable" 16 | skipVariableArgName = "variables" 17 | ) 18 | 19 | type skipVariable struct{} 20 | 21 | func (s *skipVariable) Directive() *ast.DirectiveDefinition { 22 | return &ast.DirectiveDefinition{ 23 | IsRepeatable: true, 24 | Description: prependMockWorked(appendIfExistExampleGraphql(i18n.SkipVariableDesc.String())), 25 | Name: skipVariableName, 26 | Locations: []ast.DirectiveLocation{ast.LocationField}, 27 | Arguments: ast.ArgumentDefinitionList{{ 28 | Name: skipVariableArgName, 29 | Type: ast.NonNullListType(&ast.Type{NamedType: consts.ScalarString}, nil), 30 | }, { 31 | Name: argIfRuleName, 32 | Type: ast.NonNullNamedType(consts.ScalarString, nil), 33 | }}, 34 | } 35 | } 36 | 37 | func (s *skipVariable) Definitions() ast.DefinitionList { 38 | return nil 39 | } 40 | 41 | func (s *skipVariable) Resolve(resolver *SelectionResolver) (err error) { 42 | return includeDirective.Resolve(resolver) 43 | } 44 | 45 | func init() { 46 | registerDirective(skipVariableName, &skipVariable{}) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/engine/directives/variable_fromheader.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现VariableDirective接口,只能定义在LocationVariableDefinition上 4 | Resolve 按照引擎需要的配置定义VariablesConfiguration.InjectVariables,留作后续发送graphql前填充请求头中参数 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/plugins/i18n" 11 | "fmt" 12 | "github.com/vektah/gqlparser/v2/ast" 13 | "github.com/wundergraph/wundergraph/pkg/wgpb" 14 | ) 15 | 16 | const fromHeaderName = "fromHeader" 17 | 18 | type fromHeader struct{} 19 | 20 | func (v *fromHeader) Directive() *ast.DirectiveDefinition { 21 | return &ast.DirectiveDefinition{ 22 | Description: appendIfExistExampleGraphql(i18n.FromHeaderDesc.String()), 23 | Name: fromHeaderName, 24 | Locations: []ast.DirectiveLocation{ast.LocationVariableDefinition}, 25 | Arguments: ast.ArgumentDefinitionList{{ 26 | Name: commonArgName, 27 | Type: ast.NonNullNamedType(consts.ScalarString, nil), 28 | }}, 29 | } 30 | } 31 | 32 | func (v *fromHeader) Definitions() ast.DefinitionList { 33 | return nil 34 | } 35 | 36 | func (v *fromHeader) Resolve(resolver *VariableResolver) (_, skip bool, err error) { 37 | value, ok := resolver.Arguments[commonArgName] 38 | if !ok { 39 | err = fmt.Errorf(argumentRequiredFormat, commonArgName) 40 | return 41 | } 42 | 43 | resolver.Operation.VariablesConfiguration.InjectVariables = append(resolver.Operation.VariablesConfiguration.InjectVariables, &wgpb.VariableInjectionConfiguration{ 44 | VariablePathComponents: resolver.Path, 45 | VariableKind: wgpb.InjectVariableKind_FROM_HEADER, 46 | ValueTypeName: getRealVariableTypeName(resolver.Schema), 47 | FromHeaderName: value, 48 | }) 49 | skip = true 50 | return 51 | } 52 | 53 | func init() { 54 | registerDirective(fromHeaderName, &fromHeader{}) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/engine/directives/variable_hookvariable.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现VariableDirective接口,只能定义在LocationVariableDefinition上 4 | Resolve 目前空置为提供支持(主要未解决不存在参数的判断问题) 5 | */ 6 | package directives 7 | 8 | import ( 9 | "github.com/vektah/gqlparser/v2/ast" 10 | "github.com/wundergraph/wundergraph/pkg/apihandler" 11 | ) 12 | 13 | const hookVariableName = "hookVariable" 14 | 15 | type hookVariable struct{ variableDirectiveRemoved } 16 | 17 | func (v *hookVariable) Directive() *ast.DirectiveDefinition { 18 | return &ast.DirectiveDefinition{ 19 | Name: hookVariableName, 20 | Locations: []ast.DirectiveLocation{ast.LocationVariableDefinition}, 21 | Description: appendIfExistExampleGraphql(""), 22 | } 23 | } 24 | 25 | func (v *hookVariable) Definitions() ast.DefinitionList { 26 | return nil 27 | } 28 | 29 | func (v *hookVariable) Resolve(*VariableResolver) (bool, bool, error) { 30 | return false, false, nil 31 | } 32 | 33 | func init() { 34 | registerDirective(hookVariableName, &hookVariable{}) 35 | apihandler.AddClearVariableDirectiveName(hookVariableName) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/engine/directives/variable_injectenvironmentvariable.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现VariableDirective接口,只能定义在LocationVariableDefinition上 4 | Resolve 按照引擎需要的配置定义VariablesConfiguration.InjectVariables,留作后续发送graphql前填充环境变量参数 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/plugins/i18n" 11 | "fmt" 12 | "github.com/vektah/gqlparser/v2/ast" 13 | "github.com/wundergraph/wundergraph/pkg/wgpb" 14 | ) 15 | 16 | const injectEnvironmentVariableName = "injectEnvironmentVariable" 17 | 18 | type injectEnvironmentVariable struct{} 19 | 20 | func (v *injectEnvironmentVariable) Directive() *ast.DirectiveDefinition { 21 | return &ast.DirectiveDefinition{ 22 | Description: appendIfExistExampleGraphql(i18n.InjectEnvironmentVariableDesc.String()), 23 | Name: injectEnvironmentVariableName, 24 | Locations: []ast.DirectiveLocation{ast.LocationVariableDefinition}, 25 | Arguments: ast.ArgumentDefinitionList{{ 26 | Name: commonArgName, 27 | Type: ast.NonNullNamedType(consts.ScalarString, nil), 28 | }}, 29 | } 30 | } 31 | 32 | func (v *injectEnvironmentVariable) Definitions() ast.DefinitionList { 33 | return nil 34 | } 35 | 36 | func (v *injectEnvironmentVariable) Resolve(resolver *VariableResolver) (_, skip bool, err error) { 37 | value, ok := resolver.Arguments[commonArgName] 38 | if !ok { 39 | err = fmt.Errorf(argumentRequiredFormat, commonArgName) 40 | return 41 | } 42 | 43 | resolver.Operation.VariablesConfiguration.InjectVariables = append(resolver.Operation.VariablesConfiguration.InjectVariables, &wgpb.VariableInjectionConfiguration{ 44 | VariablePathComponents: resolver.Path, 45 | VariableKind: wgpb.InjectVariableKind_ENVIRONMENT_VARIABLE, 46 | ValueTypeName: getRealVariableTypeName(resolver.Schema), 47 | EnvironmentVariableName: value, 48 | }) 49 | skip = true 50 | return 51 | } 52 | 53 | func init() { 54 | registerDirective(injectEnvironmentVariableName, &injectEnvironmentVariable{}) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/engine/directives/variable_injectgenerateduuid.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现VariableDirective接口,只能定义在LocationVariableDefinition上 4 | Resolve 按照引擎需要的配置定义VariablesConfiguration.InjectVariables,留作后续发送graphql前填充UUID参数 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/plugins/i18n" 10 | "github.com/vektah/gqlparser/v2/ast" 11 | "github.com/wundergraph/wundergraph/pkg/wgpb" 12 | ) 13 | 14 | const injectGeneratedUUIDName = "injectGeneratedUUID" 15 | 16 | type injectGeneratedUUID struct{} 17 | 18 | func (v *injectGeneratedUUID) Directive() *ast.DirectiveDefinition { 19 | return &ast.DirectiveDefinition{ 20 | Description: appendIfExistExampleGraphql(i18n.InjectGeneratedUUIDDesc.String()), 21 | Name: injectGeneratedUUIDName, 22 | Locations: []ast.DirectiveLocation{ast.LocationVariableDefinition}, 23 | } 24 | } 25 | 26 | func (v *injectGeneratedUUID) Definitions() ast.DefinitionList { 27 | return nil 28 | } 29 | 30 | func (v *injectGeneratedUUID) Resolve(resolver *VariableResolver) (_, skip bool, err error) { 31 | resolver.Operation.VariablesConfiguration.InjectVariables = append(resolver.Operation.VariablesConfiguration.InjectVariables, &wgpb.VariableInjectionConfiguration{ 32 | VariablePathComponents: resolver.Path, 33 | VariableKind: wgpb.InjectVariableKind_UUID, 34 | ValueTypeName: getRealVariableTypeName(resolver.Schema), 35 | }) 36 | skip = true 37 | return 38 | } 39 | 40 | func init() { 41 | registerDirective(injectGeneratedUUIDName, &injectGeneratedUUID{}) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/engine/directives/variable_internal.go: -------------------------------------------------------------------------------- 1 | // Package directives 2 | /* 3 | 实现VariableDirective接口,只能定义在LocationVariableDefinition上 4 | Resolve 标识参数内部传递,与@export组合使用 5 | */ 6 | package directives 7 | 8 | import ( 9 | "fireboom-server/pkg/plugins/i18n" 10 | "github.com/vektah/gqlparser/v2/ast" 11 | ) 12 | 13 | const internalName = "internal" 14 | 15 | type internal struct{} 16 | 17 | func (v *internal) Directive() *ast.DirectiveDefinition { 18 | return &ast.DirectiveDefinition{ 19 | Description: prependMockWorked(appendIfExistExampleGraphql(i18n.InternalDesc.String())), 20 | Name: internalName, 21 | Locations: []ast.DirectiveLocation{ast.LocationVariableDefinition}, 22 | } 23 | } 24 | 25 | func (v *internal) Definitions() ast.DefinitionList { 26 | return nil 27 | } 28 | 29 | func (v *internal) Resolve(resolver *VariableResolver) (bool, bool, error) { 30 | _, exported := resolver.VariableExported[resolver.Path[0]] 31 | return exported, true, nil 32 | } 33 | 34 | func init() { 35 | registerDirective(internalName, &internal{}) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/engine/sdk/hook_swagger.go: -------------------------------------------------------------------------------- 1 | // Package sdk 2 | /* 3 | 钩子swagger文档,可以根据此输出自定义开发sdk服务 4 | */ 5 | package sdk 6 | 7 | import ( 8 | "fireboom-server/pkg/common/configs" 9 | "fireboom-server/pkg/common/consts" 10 | "fireboom-server/pkg/common/utils" 11 | "fireboom-server/pkg/engine/build" 12 | "fireboom-server/pkg/plugins/fileloader" 13 | "github.com/getkin/kin-openapi/openapi3" 14 | json "github.com/json-iterator/go" 15 | "github.com/wundergraph/wundergraph/pkg/interpolate" 16 | "golang.org/x/exp/maps" 17 | "golang.org/x/exp/slices" 18 | ) 19 | 20 | func (o *reflectObjectFactory) generateHookSwagger() { 21 | doc := &openapi3.T{ 22 | OpenAPI: "3.0.1", 23 | Security: make(openapi3.SecurityRequirements, 0), 24 | Paths: make(openapi3.Paths), 25 | Info: &openapi3.Info{ 26 | Title: "Fireboom Hook swagger3.0", 27 | Contact: &openapi3.Contact{URL: configs.ApplicationData.ContactAddress}, 28 | Version: utils.GetStringWithLockViper(consts.FbVersion), 29 | }, 30 | Components: &openapi3.Components{Schemas: o.definitions}, 31 | } 32 | 33 | endpointKeys := maps.Keys(o.endpoints) 34 | slices.Sort(endpointKeys) 35 | for _, endpoint := range endpointKeys { 36 | metadata := o.endpoints[endpoint] 37 | var ( 38 | requestBodySchema, responseSchema *openapi3.SchemaRef 39 | requestBodyOk, responseOk bool 40 | ) 41 | if metadata.requestBody != nil { 42 | requestBodyName := utils.GetTypeName(metadata.requestBody) 43 | if _, requestBodyOk = o.definitions[requestBodyName]; requestBodyOk { 44 | requestBodySchema = &openapi3.SchemaRef{Ref: interpolate.Openapi3SchemaRefPrefix + requestBodyName} 45 | } 46 | } 47 | if metadata.response != nil { 48 | responseName := utils.GetTypeName(metadata.response) 49 | if responseSchema, responseOk = o.definitions[responseName]; responseOk { 50 | if metadata.middlewareResponseRewrite != nil { 51 | rewriteName := utils.GetTypeName(metadata.middlewareResponseRewrite) 52 | copySchema := *responseSchema 53 | copySchema.Value.Properties["response"] = o.definitions[rewriteName] 54 | responseSchema = ©Schema 55 | } else { 56 | responseSchema = &openapi3.SchemaRef{Ref: interpolate.Openapi3SchemaRefPrefix + responseName} 57 | } 58 | } 59 | } 60 | pathItem := &openapi3.Operation{ 61 | OperationID: string(metadata.title), 62 | Summary: endpoint, 63 | Security: openapi3.NewSecurityRequirements(), 64 | } 65 | if requestBodyOk { 66 | pathItem.RequestBody = utils.MakeApiOperationRequestBody(requestBodySchema) 67 | } 68 | if responseOk { 69 | pathItem.Responses = utils.MakeApiOperationResponse(responseSchema) 70 | } 71 | doc.Paths[endpoint] = &openapi3.PathItem{Post: pathItem} 72 | } 73 | 74 | docBytes, _ := json.Marshal(&doc) 75 | _ = build.GeneratedHookSwaggerText.Write(build.GeneratedHookSwaggerText.Title, fileloader.SystemUser, docBytes) 76 | } 77 | -------------------------------------------------------------------------------- /pkg/engine/sdk/template_eventbus.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import ( 4 | "fireboom-server/pkg/common/models" 5 | "github.com/wundergraph/wundergraph/pkg/eventbus" 6 | ) 7 | 8 | func (t *templateContext) Subscribe() { 9 | eventbus.Subscribe(eventbus.ChannelSdk, eventbus.EventInsert, func(data any) any { 10 | go t.generateTemplate(data.(*models.Sdk)) 11 | return nil 12 | }) 13 | eventbus.Subscribe(eventbus.ChannelSdk, eventbus.EventUpdate, func(data any) any { 14 | go t.generateTemplate(data.(*models.Sdk)) 15 | return nil 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/engine/server/build.go: -------------------------------------------------------------------------------- 1 | // Package server 2 | /* 3 | 引擎编译功能的实现 4 | CallBuildResolves调用build包下注册的编译函数,例如数据源、接口、上传、认证等 5 | CallAsyncGenerates异步调用生成函数,例如swagger、sdk 6 | */ 7 | package server 8 | 9 | import ( 10 | "fireboom-server/pkg/common/configs" 11 | "fireboom-server/pkg/common/consts" 12 | "fireboom-server/pkg/common/utils" 13 | "fireboom-server/pkg/engine/build" 14 | _ "fireboom-server/pkg/engine/sdk" 15 | _ "fireboom-server/pkg/engine/swagger" 16 | "github.com/wundergraph/wundergraph/pkg/eventbus" 17 | "github.com/wundergraph/wundergraph/pkg/wgpb" 18 | "go.uber.org/zap" 19 | "sync" 20 | ) 21 | 22 | var EngineBuilder *EngineBuild 23 | 24 | func init() { 25 | utils.RegisterInitMethod(30, func() { 26 | EngineBuilder = &EngineBuild{logger: zap.L()} 27 | }) 28 | } 29 | 30 | type EngineBuild struct { 31 | logger *zap.Logger 32 | builder *build.Builder 33 | } 34 | 35 | func (b *EngineBuild) release() { 36 | build.GeneratedGraphqlConfigRoot.ClearCache() 37 | if b.builder != nil { 38 | b.builder.FieldHashes.Clear() 39 | b.builder = nil 40 | } 41 | } 42 | 43 | // GenerateGraphqlConfig 编译并生成引擎所需配置文件 44 | // group 在build命令下会传入,会等待CallAsyncGenerates全部完成后结束进程 45 | func (b *EngineBuild) GenerateGraphqlConfig(group ...*sync.WaitGroup) (err error) { 46 | defer func() { 47 | if err != nil { 48 | b.logger.Error("build failed", zap.String(consts.EngineStatusField, consts.EngineBuildFailed), zap.Error(err)) 49 | } 50 | }() 51 | 52 | b.logger.Info("build begin", zap.String(consts.EngineStatusField, consts.EngineBuilding), zap.String("envEffective", configs.EnvEffectiveRoot.GetPath())) 53 | b.initDefinedApi() 54 | if err = build.CallRunResolves(b.builder); err != nil { 55 | return 56 | } 57 | 58 | if err = b.emitGraphqlConfigCache(); err != nil { 59 | return 60 | } 61 | 62 | build.CallAsyncGenerates(b.builder, group...) 63 | eventbus.EnsureEventSubscribe(b) 64 | b.logger.Info("build finish", zap.String(consts.EngineStatusField, consts.EngineBuildSucceed)) 65 | return 66 | } 67 | 68 | func (b *EngineBuild) initDefinedApi() { 69 | setting := configs.GlobalSettingRoot.FirstData() 70 | b.builder = &build.Builder{DefinedApi: &wgpb.UserDefinedApi{ 71 | NodeOptions: setting.NodeOptions, 72 | ServerOptions: setting.ServerOptions, 73 | CorsConfiguration: setting.CorsConfiguration, 74 | EnableGraphqlEndpoint: true, 75 | AllowedHostNames: setting.AllowedHostNames, 76 | }} 77 | } 78 | 79 | func (b *EngineBuild) emitGraphqlConfigCache() error { 80 | graphqlConfig := &wgpb.WunderGraphConfiguration{Api: b.builder.DefinedApi} 81 | return build.GeneratedGraphqlConfigRoot.InsertOrUpdate(graphqlConfig) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/engine/swagger/api_authentication.go: -------------------------------------------------------------------------------- 1 | // Package swagger 2 | /* 3 | 添加身份认证接口文档 4 | */ 5 | package swagger 6 | 7 | import ( 8 | "fireboom-server/pkg/common/utils" 9 | "github.com/getkin/kin-openapi/openapi3" 10 | "github.com/wundergraph/wundergraph/pkg/authentication" 11 | ) 12 | 13 | func (s *document) buildApiAuthentication() { 14 | userSchemaRefName := utils.ReflectStructToOpenapi3Schema(authentication.User{}, s.doc.Components.Schemas) 15 | userSchema := s.doc.Components.Schemas[userSchemaRefName] 16 | delete(s.doc.Components.Schemas, userSchemaRefName) 17 | userTags := []string{"Platform-User"} 18 | userInfoUri := "/auth/cookie/user" 19 | s.doc.Paths[userInfoUri] = &openapi3.PathItem{ 20 | Get: &openapi3.Operation{ 21 | Tags: userTags, 22 | Summary: "获取用户信息", 23 | OperationID: userInfoUri, 24 | Security: &s.doc.Security, 25 | Responses: utils.MakeApiOperationResponse(userSchema), 26 | }, 27 | } 28 | 29 | logoutParam := openapi3.NewQueryParameter("logout_openid_connect_provider") 30 | var paramEnum []interface{} 31 | paramEnum = append(paramEnum, "true", "false") 32 | logoutParam.WithSchema(&openapi3.Schema{Type: openapi3.TypeString, Enum: paramEnum}) 33 | userLogoutUri := "/auth/cookie/user/logout" 34 | s.doc.Paths[userLogoutUri] = &openapi3.PathItem{ 35 | Get: &openapi3.Operation{ 36 | Tags: userTags, 37 | Summary: "用户登出", 38 | OperationID: userLogoutUri, 39 | Security: &s.doc.Security, 40 | Parameters: openapi3.Parameters{{Value: logoutParam}}, 41 | Responses: openapi3.Responses{}, 42 | }, 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/plugins/embed/code.go: -------------------------------------------------------------------------------- 1 | // Package embed 2 | /* 3 | 嵌入资源管理 4 | DefaultFs 系统默认配置,包括env, globalOperation, globalSetting, license 5 | TemplateFs 模板配置,用作渲染默认配置的初始化模板 6 | ResourceFs 资源配置, 包括banner图,application设置,内省查询参数 7 | DatasourceFs 内置数据源 8 | RoleFs 内置角色 9 | DirectiveExampleFs graphql指令示例 10 | */ 11 | package embed 12 | 13 | import ( 14 | "embed" 15 | ) 16 | 17 | var ( 18 | //go:embed default/* 19 | DefaultFs embed.FS 20 | //go:embed resource/* 21 | ResourceFs embed.FS 22 | //go:embed datasource/* 23 | DatasourceFs embed.FS 24 | //go:embed role/* 25 | RoleFs embed.FS 26 | //go:embed directive_example/* 27 | DirectiveExampleFs embed.FS 28 | ) 29 | 30 | const ( 31 | DefaultRoot = "default" 32 | ResourceRoot = "resource" 33 | DatasourceRoot = "datasource" 34 | RoleRoot = "role" 35 | DirectiveExampleRoot = "directive_example" 36 | ) 37 | -------------------------------------------------------------------------------- /pkg/plugins/embed/datasource/system.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "system", 3 | "enabled": true, 4 | "kind": 1, 5 | "customRest": { 6 | "oasFilepath": "system.json", 7 | "baseUrl": { 8 | "staticVariableContent": "http://localhost:9123/api" 9 | }, 10 | "headers": { 11 | "X-FB-Authentication": { 12 | "values": [ 13 | { 14 | "kind": 1, 15 | "environmentVariableName": "X_FB_Authentication" 16 | } 17 | ] 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/default/.env: -------------------------------------------------------------------------------- 1 | FB_API_INTERNAL_URL="http://localhost:9991" 2 | FB_API_PUBLIC_URL="http://localhost:9991" 3 | FB_API_LISTEN_HOST="0.0.0.0" 4 | FB_API_LISTEN_PORT="9991" 5 | FB_LOG_LEVEL="debug" 6 | FB_SERVER_URL="http://localhost:9992" 7 | FB_SERVER_LISTEN_HOST="localhost" 8 | FB_SERVER_LISTEN_PORT="9992" 9 | FB_CSRF_TOKEN_SECRET="XgloxMqlZQD" 10 | FB_SECURE_COOKIE_BLOCK_KEY="fJkYInBRWRiGlflRKJeMsSpGSHEvJthk" 11 | FB_SECURE_COOKIE_HASH_KEY="sHwzzwQBZUOQQGbdvcSQxUXsPMzuJGmh" 12 | FB_REPO_URL_MIRROR="https://git.fireboom.io/{orgName}/{repoName}.git" 13 | FB_RAW_URL_MIRROR="https://raw.git.fireboom.io/{orgName}/{repoName}/{branchName}/{filePath}" 14 | FB_FILES_URL="https://files.fireboom.io" -------------------------------------------------------------------------------- /pkg/plugins/embed/default/.env.prod: -------------------------------------------------------------------------------- 1 | FB_API_INTERNAL_URL="http://localhost:9991" 2 | FB_API_PUBLIC_URL="http://localhost:9991" 3 | FB_API_LISTEN_HOST="0.0.0.0" 4 | FB_API_LISTEN_PORT="9991" 5 | FB_LOG_LEVEL="info" 6 | FB_SERVER_URL="http://localhost:9992" 7 | FB_SERVER_LISTEN_HOST="0.0.0.0" 8 | FB_SERVER_LISTEN_PORT="9992" 9 | FB_CSRF_TOKEN_SECRET="XgloxMqlZQD" 10 | FB_SECURE_COOKIE_BLOCK_KEY="fJkYInBRWRiGlflRKJeMsSpGSHEvJthk" 11 | FB_SECURE_COOKIE_HASH_KEY="sHwzzwQBZUOQQGbdvcSQxUXsPMzuJGmh" 12 | FB_REPO_URL_MIRROR="https://git.fireboom.io/{orgName}/{repoName}.git" 13 | FB_RAW_URL_MIRROR="https://raw.git.fireboom.io/{orgName}/{repoName}/{branchName}/{filePath}" 14 | FB_FILES_URL="https://files.fireboom.io" -------------------------------------------------------------------------------- /pkg/plugins/embed/default/global.operation.json: -------------------------------------------------------------------------------- 1 | { 2 | "cacheConfig": { 3 | "maxAge": 120, 4 | "staleWhileRevalidate": 30 5 | }, 6 | "liveQueryConfig": { 7 | "pollingIntervalSeconds": 10 8 | } 9 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/default/global.setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeOptions": { 3 | "nodeUrl": { 4 | "kind": 1, 5 | "environmentVariableName": "FB_API_INTERNAL_URL" 6 | }, 7 | "publicNodeUrl": { 8 | "kind": 1, 9 | "environmentVariableName": "FB_API_PUBLIC_URL" 10 | }, 11 | "listen": { 12 | "host": { 13 | "kind": 1, 14 | "environmentVariableName": "FB_API_LISTEN_HOST" 15 | }, 16 | "port": { 17 | "kind": 1, 18 | "environmentVariableName": "FB_API_LISTEN_PORT" 19 | } 20 | }, 21 | "logger": { 22 | "level": { 23 | "kind": 1, 24 | "environmentVariableName": "FB_LOG_LEVEL" 25 | } 26 | } 27 | }, 28 | "serverOptions": { 29 | "serverUrl": { 30 | "kind": 1, 31 | "environmentVariableName": "FB_SERVER_URL" 32 | }, 33 | "listen": { 34 | "host": { 35 | "kind": 1, 36 | "environmentVariableName": "FB_SERVER_LISTEN_HOST" 37 | }, 38 | "port": { 39 | "kind": 1, 40 | "environmentVariableName": "FB_SERVER_LISTEN_PORT" 41 | } 42 | } 43 | }, 44 | "corsConfiguration": { 45 | "allowedOrigins": [ 46 | { 47 | "staticVariableContent": "*" 48 | } 49 | ], 50 | "allowedMethods": [ 51 | "GET", 52 | "POST" 53 | ], 54 | "allowedHeaders": [ 55 | "*" 56 | ], 57 | "exposedHeaders": [ 58 | "*" 59 | ], 60 | "maxAge": 120, 61 | "allowCredentials": true 62 | }, 63 | "allowedHostNames": [ 64 | { 65 | "staticVariableContent": "*" 66 | } 67 | ], 68 | "authorizedRedirectUris": [ 69 | { 70 | "staticVariableContent": "http://localhost:9123/#/workbench/userInfo" 71 | }, 72 | { 73 | "staticVariableContent": "http://localhost:9123/#/workbench/rapi/loginBack" 74 | } 75 | ], 76 | "enableCSRFProtect": true, 77 | "consoleLogger": { 78 | "filename": "log/fireboom.log", 79 | "maxage": 7, 80 | "maxsize": 10, 81 | "compress": true 82 | }, 83 | "buildInfo": { 84 | "BuiltBy": "Fireboom" 85 | } 86 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/default/jaeger.config.yml: -------------------------------------------------------------------------------- 1 | serviceName: "FireboomNode.${active:dev}" 2 | disabled: true 3 | sampler: 4 | type: const 5 | param: 1 6 | reporter: 7 | queueSize: 100 8 | bufferFlushInterval: 1000000000 9 | localAgentHostPort: "127.0.0.1:6831" -------------------------------------------------------------------------------- /pkg/plugins/embed/default/license.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultLimits": { 3 | "datasource": 25, 4 | "operation": 1000, 5 | "import": 0, 6 | "teamwork": 0, 7 | "incrementBuild": 0 8 | }, 9 | "wsPushRequired": [ 10 | "datasource", 11 | "operation", 12 | "prismaDatasource" 13 | ] 14 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/operation_internaloperation.graphql: -------------------------------------------------------------------------------- 1 | query myQuery @internalOperation { 2 | data: findFirstStudent { 3 | userId 4 | name 5 | } 6 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/operation_rbac.graphql: -------------------------------------------------------------------------------- 1 | query myQuery @rbac(requireMatchAny: [admin, user]) { 2 | data: findFirstStudent { 3 | userId 4 | name 5 | } 6 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/operation_transaction.graphql: -------------------------------------------------------------------------------- 1 | mutation myQuery ($requestTime: DateTime! @injectCurrentDateTime(format: ISO8601)) @transaction(maxWaitSeconds: 10, timeoutSeconds: 10, isolationLevel: read_committed) { 2 | appLog: createOneLog(data: {requestTime: $requestTime, from: "app"}) { 3 | id 4 | } 5 | reqeustLog: createOneLog(data: {requestTime: $requestTime, from: "request"}) { 6 | id 7 | } 8 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/selection_export.graphql: -------------------------------------------------------------------------------- 1 | query myQuery ($userId: ID!, $classId: String! @internal) { 2 | data: findFirstStudent(where: {userId: {equals: $userId}}) { 3 | userId 4 | name 5 | classId @export(as: "classId") 6 | teacher: _join { 7 | findManyTeacher(where: {classId: {equals: $classId}}) { 8 | name 9 | subject 10 | } 11 | } 12 | class: _join_mutation { 13 | updateManyClass(where: {classId: {equals: $classId}}, data: {deletedAt: {set: null}}) { 14 | count 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/selection_formatdatetime.graphql: -------------------------------------------------------------------------------- 1 | query myQuery { 2 | data: findManyStudent { 3 | userId 4 | name 5 | birthday @formatDateTime(customFormat: "yyyy-MM-dd") 6 | } 7 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/selection_transform.graphql: -------------------------------------------------------------------------------- 1 | query myQuery ($userId: ID!, $classId: String! @internal) { 2 | data: findFirstStudent(where: {userId: {equals: $userId}}) { 3 | userId 4 | name 5 | classId @export(as: "classId") 6 | teacher: _join @transform(get: "findManyTeacher") { 7 | findManyTeacher(where: {classId: {equals: $classId}}) { 8 | name 9 | subject 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/variable_fromclaim.graphql: -------------------------------------------------------------------------------- 1 | query myQuery ($userId: String! @fromClaim(name: USERID), $name: String! @fromClaim(name: NAME), 2 | $now: DateTime @injectCurrentDateTime(format: ISO8601, offset: {value: 100, unit: YEAR}) 3 | ) { 4 | data: findFirstStudent(where: {OR: [{userId: {equals: $userId}, name: {equals: $name}}, {createdAt: {lte: $now}}]}) { 5 | userId 6 | name 7 | } 8 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/variable_fromheader.graphql: -------------------------------------------------------------------------------- 1 | mutation myQuery ($requestId: String! @fromHeader(name: "x-request-id"), $requestId: String! @fromHeader(name: "host")) { 2 | data: createOneLog(data: {requestId: $requestId, host: $host, from: "request"}) { 3 | id 4 | } 5 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/variable_hookvariable.graphql: -------------------------------------------------------------------------------- 1 | query myQuery ($filter: String! @hookVariable) { 2 | data: findFirstStudent { 3 | userId 4 | name 5 | } 6 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/variable_injectcurrentdatetime.graphql: -------------------------------------------------------------------------------- 1 | mutation myQuery ($requestId: String! @fromHeader(name: "x-request-id"), $requestTime: DateTime! @injectCurrentDateTime(format: ISO8601)) { 2 | data: createOneLog(data: {requestId: $requestId, requestTime: $requestTime, from: "request"}) { 3 | id 4 | } 5 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/variable_injectenvironmentvariable.graphql: -------------------------------------------------------------------------------- 1 | mutation myQuery ($appId: String! @injectEnvironmentVariable(name: "app_id"), $requestTime: DateTime! @injectCurrentDateTime(format: ISO8601)) { 2 | data: createOneLog(data: {appId: $appId, requestTime: $requestTime, from: "app"}) { 3 | id 4 | } 5 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/variable_injectgenerateduuid.graphql: -------------------------------------------------------------------------------- 1 | mutation myQuery ($id: ID! @injectGeneratedUUID, $requestTime: DateTime! @injectCurrentDateTime(format: ISO8601)) { 2 | data: createOneLog(data: {id: $appId, requestTime: $requestTime, from: "app"}) { 3 | id 4 | } 5 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/variable_internal.graphql: -------------------------------------------------------------------------------- 1 | query myQuery ($userId: ID!, $classId: String! @internal) { 2 | data: findFirstStudent(where: {userId: {equals: $userId}}) { 3 | userId 4 | name 5 | classId @export(as: "classId") 6 | teacher: _join { 7 | findManyTeacher(where: {classId: {equals: $classId}}) { 8 | name 9 | subject 10 | } 11 | } 12 | class: _join_mutation { 13 | updateManyClass(where: {classId: {equals: $classId}}, data: {deletedAt: {set: null}}) { 14 | count 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/variable_jsonschema.graphql: -------------------------------------------------------------------------------- 1 | query myQuery ($email: String! @jsonSchema( 2 | title: "Message" 3 | description: "Describe the message" 4 | pattern: "^[a-zA-Z 0-9]+$" 5 | commonPattern: EMAIL 6 | commonPattern: DOMAIN 7 | minLength: 3 8 | maxLength: 5 9 | minimum: 1 10 | maximum: 1 11 | exclusiveMaximum: 2 12 | exclusiveMinimum: 2 13 | maxItems: 1 14 | minItems: 1 15 | multipleOf: 1 16 | uniqueItems: true)) { 17 | data: findFirstStudent(where: {email: {equals: $email}}) { 18 | userId 19 | name 20 | } 21 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/directive_example/variable_whereinput.graphql: -------------------------------------------------------------------------------- 1 | query MyQuery($address: String, $OR: [admin_demo_areaWhereInput]! = [{id: {equals: 0}}] @fromClaim(name: CUSTOM, customJsonPath: ["areaCodes"]) @whereInput(filter: {field: "code", scalar: {type: startsWith}}), $skip: Int = 0, $take: Int = 10) { 2 | data: admin_findManydemo_area( 3 | where: {address: {contains: $address}, OR: $OR} 4 | skip: $skip 5 | take: $take 6 | ) { 7 | address 8 | code 9 | createdAt 10 | id 11 | name 12 | parentCode 13 | } 14 | total: admin_aggregatedemo_area( 15 | where: {address: {contains: $address}, OR: $OR} 16 | ) @transform(get: "_count.id") { 17 | _count { 18 | id 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/plugins/embed/resource/application.properties: -------------------------------------------------------------------------------- 1 | context.path=/api 2 | request.logger.skippers=/assets,/iconfont,/modules,/gifs,/*,favicon.ico,/swagger/ 3 | authentication.urls=/api,/ws 4 | engine.forward.requests=/app/main/graphql 5 | contact.address=https://www.fireboom.io/ -------------------------------------------------------------------------------- /pkg/plugins/embed/resource/banner.txt: -------------------------------------------------------------------------------- 1 | 2 | ███████╗██╗██████╗ ███████╗██████╗ ██████╗ ██████╗ ███╗ ███╗ 3 | ██╔════╝██║██╔══██╗██╔════╝██╔══██╗██╔═══██╗██╔═══██╗████╗ ████║ 4 | █████╗ ██║██████╔╝█████╗ ██████╔╝██║ ██║██║ ██║██╔████╔██║ 5 | ██╔══╝ ██║██╔══██╗██╔══╝ ██╔══██╗██║ ██║██║ ██║██║╚██╔╝██║ 6 | ██║ ██║██║ ██║███████╗██████╔╝╚██████╔╝╚██████╔╝██║ ╚═╝ ██║ 7 | ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ 8 | -------------------------------------------------------------------------------- /pkg/plugins/embed/resource/introspect.json: -------------------------------------------------------------------------------- 1 | { 2 | "query": "\n query IntrospectionQuery {\n __schema {\n \n queryType { name }\n mutationType { name }\n subscriptionType { name }\n types {\n ...FullType\n }\n directives {\n name\n description\n \n locations\n args {\n ...InputValue\n }\n }\n }\n }\n\n fragment FullType on __Type {\n kind\n name\n description\n \n fields(includeDeprecated: true) {\n name\n description\n args {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n description\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n }\n\n fragment InputValue on __InputValue {\n name\n description\n type { ...TypeRef }\n defaultValue\n \n \n }\n\n fragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n }\n ", 3 | "operationName": "IntrospectionQuery" 4 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/role/admin.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": "admin" 3 | } -------------------------------------------------------------------------------- /pkg/plugins/embed/role/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "code": "user" 3 | } -------------------------------------------------------------------------------- /pkg/plugins/fileloader/extension.go: -------------------------------------------------------------------------------- 1 | // Package fileloader 2 | /* 3 | 文件加载的扩展名和目录字典 4 | */ 5 | package fileloader 6 | 7 | type Extension string 8 | 9 | const ( 10 | ExtJson Extension = ".json" 11 | ExtTxt Extension = ".txt" 12 | ExtProperties Extension = ".properties" 13 | ExtGraphql Extension = ".graphql" 14 | ExtPrisma Extension = ".prisma" 15 | ExtYaml Extension = ".yaml" 16 | ExtYml Extension = ".yml" 17 | ExtKey Extension = ".key" 18 | ) 19 | 20 | var rootDirectories map[string]string 21 | 22 | func GetRootDirectories() map[string]string { 23 | return rootDirectories 24 | } 25 | 26 | func init() { 27 | rootDirectories = make(map[string]string) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/plugins/fileloader/model_data_tree.go: -------------------------------------------------------------------------------- 1 | // Package fileloader 2 | /* 3 | 目录树实现,返回数据包括全路径、节点名、是否目录、扩展名,子节点等 4 | */ 5 | package fileloader 6 | 7 | import ( 8 | "fireboom-server/pkg/plugins/i18n" 9 | "github.com/spf13/cast" 10 | "golang.org/x/exp/slices" 11 | "io/fs" 12 | "path/filepath" 13 | "strings" 14 | ) 15 | 16 | type ( 17 | DataTree struct { 18 | Name string `json:"name"` 19 | Path string `json:"path"` 20 | IsDir bool `json:"isDir"` 21 | Extension string `json:"extension,omitempty"` 22 | Items DataTrees `json:"items,omitempty"` 23 | Extra any `json:"extra,omitempty"` 24 | } 25 | DataTrees []*DataTree 26 | ) 27 | 28 | // GetDataTrees 返回目录树结构数据,仅multipleRW支持目录树结构 29 | // 返回数据会按照目录优先,名称其次的顺序对结果排序(每层均有序) 30 | func (p *Model[T]) GetDataTrees() (trees DataTrees, err error) { 31 | if p.rwType != multipleRW { 32 | err = i18n.NewCustomErrorWithMode(p.modelName, nil, i18n.LoaderMultipleOnlyError) 33 | return 34 | } 35 | 36 | dirTreeMap := make(map[string]*DataTree) 37 | _ = filepath.Walk(p.Root, func(path string, info fs.FileInfo, err error) error { 38 | if info == nil || path == p.Root { 39 | return nil 40 | } 41 | 42 | basename := filepath.Base(path) 43 | if info.IsDir() && strings.HasPrefix(basename, ".") { 44 | return nil 45 | } 46 | current := &DataTree{Name: basename} 47 | current.Path, _ = filepath.Rel(p.Root, path) 48 | current.Path = filepath.ToSlash(current.Path) 49 | parent, parentExist := dirTreeMap[filepath.Dir(path)] 50 | var itemAppendIgnore bool 51 | defer func() { 52 | if itemAppendIgnore { 53 | return 54 | } 55 | 56 | if parentExist { 57 | parent.Items = append(parent.Items, current) 58 | } else { 59 | trees = append(trees, current) 60 | } 61 | }() 62 | 63 | if info.IsDir() { 64 | current.IsDir = true 65 | dirTreeMap[path] = current 66 | return nil 67 | } 68 | 69 | extension := string(p.Extension) 70 | if !strings.HasSuffix(path, extension) { 71 | itemAppendIgnore = true 72 | return nil 73 | } 74 | 75 | dataName := strings.TrimSuffix(current.Path, extension) 76 | data, err := p.GetByDataName(dataName) 77 | if err != nil || !p.filter(data) { 78 | itemAppendIgnore = true 79 | return nil 80 | } 81 | 82 | current.Name = strings.TrimSuffix(current.Name, extension) 83 | current.Path = strings.TrimSuffix(current.Path, extension) 84 | current.Extension = extension 85 | if extra := p.DataTreeExtra; extra != nil { 86 | current.Extra = extra(data) 87 | } 88 | 89 | return nil 90 | }) 91 | trees.sort() 92 | return 93 | } 94 | 95 | func (d DataTrees) sort() { 96 | if len(d) == 0 { 97 | return 98 | } 99 | 100 | slices.SortFunc(d, func(a, b *DataTree) bool { 101 | aInt, bInt := cast.ToInt(!a.IsDir), cast.ToInt(!b.IsDir) 102 | 103 | return aInt < bInt || aInt == bInt && a.Name < b.Name 104 | }) 105 | for _, tree := range d { 106 | tree.Items.sort() 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /pkg/plugins/fileloader/model_text_private.go: -------------------------------------------------------------------------------- 1 | // Package fileloader 2 | /* 3 | 文本数据定义的私有方法,包括拷贝,删除,重命名等 4 | */ 5 | package fileloader 6 | 7 | import ( 8 | "fireboom-server/pkg/common/utils" 9 | "os" 10 | ) 11 | 12 | func (t *ModelText[T]) copy(srcDataName, dstDataName string) error { 13 | if t.emptyRootOrExt() { 14 | return nil 15 | } 16 | 17 | srcPath, err := t.path(srcDataName, 0) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | if utils.NotExistFile(srcPath) { 23 | return nil 24 | } 25 | 26 | dstPath, err := t.path(dstDataName, 1) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | return utils.CopyFile(srcPath, dstPath) 32 | } 33 | 34 | func (t *ModelText[T]) remove(dataName string, optional ...string) error { 35 | if t.emptyRootOrExt() { 36 | return nil 37 | } 38 | 39 | path, err := t.path(dataName, 0, optional...) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | if utils.NotExistFile(path) { 45 | return nil 46 | } 47 | 48 | return os.RemoveAll(path) 49 | } 50 | 51 | func (t *ModelText[T]) rename(srcDataName, dstDataName string, optional ...string) error { 52 | if t.emptyRootOrExt() { 53 | return nil 54 | } 55 | 56 | srcPath, err := t.path(srcDataName, 0, optional...) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | if utils.NotExistFile(srcPath) { 62 | return nil 63 | } 64 | 65 | dstPath, err := t.path(dstDataName, 1, optional...) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | return renameFile(srcPath, dstPath) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/plugins/fileloader/util.go: -------------------------------------------------------------------------------- 1 | // Package fileloader 2 | /* 3 | 文件加载插件的私有工具集 4 | */ 5 | package fileloader 6 | 7 | import ( 8 | "fireboom-server/pkg/common/utils" 9 | "github.com/asaskevich/govalidator" 10 | "os" 11 | "path/filepath" 12 | "reflect" 13 | ) 14 | 15 | func renameFile(srcPath, dstPath string) error { 16 | if err := utils.MkdirAll(filepath.Dir(dstPath)); err != nil { 17 | return err 18 | } 19 | 20 | return os.Rename(srcPath, dstPath) 21 | } 22 | 23 | func init() { 24 | // 注册自定义结构体字段校验逻辑 25 | govalidator.CustomTypeTagMap.Set("required_unless_ignored", func(i interface{}, o interface{}) bool { 26 | ignored := reflect.ValueOf(o).FieldByName("ExtensionIgnored").Bool() 27 | return ignored || !utils.IsZeroValue(i) 28 | }) 29 | govalidator.CustomTypeTagMap.Set("required_if_multiple", func(i interface{}, o interface{}) bool { 30 | rwTypeUint := reflect.ValueOf(o).FieldByName("rwType").Uint() 31 | return rwTypeUint != uint64(multipleRW) || !utils.IsZeroValue(i) 32 | }) 33 | } 34 | 35 | func filterIgnoreUnsupportedTypeError(err error) error { 36 | if err == nil { 37 | return nil 38 | } 39 | 40 | if _, match := err.(*govalidator.UnsupportedTypeError); match { 41 | return nil 42 | } 43 | 44 | errs, ok := err.(govalidator.Errors) 45 | if !ok { 46 | return err 47 | } 48 | 49 | for _, item := range errs { 50 | if err = filterIgnoreUnsupportedTypeError(item); err != nil { 51 | return err 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive.go: -------------------------------------------------------------------------------- 1 | // Package i18n 2 | /* 3 | 指令描述国际化配置,使用i18n-stringer实现 4 | */ 5 | package i18n 6 | 7 | // First check 8 | //go:generate $GOPATH/bin/i18n-stringer -type Directive -tomlpath directive -check 9 | 10 | // Second generation 11 | //go:generate $GOPATH/bin/i18n-stringer -type Directive -tomlpath directive -defaultlocale zh_cn 12 | 13 | func SwitchDirectiveLocale(locale string) bool { 14 | result := _Directive_isLocaleSupport(locale) 15 | if result { 16 | _Directive_defaultLocale = locale 17 | } 18 | return result 19 | } 20 | 21 | type Directive uint16 22 | 23 | const ExportDesc Directive = iota + 10001 24 | 25 | const ( 26 | FormatDateTimeDesc Directive = iota + 10101 27 | FormatDateTimeArgFormatDesc 28 | FormatDateTimeArgCustomFormatDesc 29 | ) 30 | 31 | const ( 32 | FromClaimDesc Directive = iota + 10201 33 | FromClaimArgNameDesc 34 | FromClaimArgCustomJsonPathDesc 35 | ) 36 | 37 | const FromHeaderDesc Directive = iota + 10301 38 | 39 | const InjectCurrentDateTimeDesc Directive = iota 40 | 41 | const InjectEnvironmentVariableDesc Directive = iota 42 | 43 | const InjectGeneratedUUIDDesc Directive = iota 44 | 45 | const InternalDesc Directive = iota + 10401 46 | 47 | const InternalOperationDesc Directive = iota + 10501 48 | 49 | const ( 50 | JsonschemaDesc Directive = iota + 10601 51 | JsonschemaArgMinimumDesc 52 | JsonschemaArgMaximumDesc 53 | JsonschemaArgMinItemsDesc 54 | JsonschemaArgMaxItemsDesc 55 | JsonschemaArgUniqueItemsDesc 56 | JsonschemaArgMaxLengthDesc 57 | JsonschemaArgMinLengthDesc 58 | JsonschemaArgPatternDesc 59 | JsonschemaArgCommonPatternDesc 60 | ) 61 | 62 | const ( 63 | RbacDesc Directive = iota + 10701 64 | RbacRequireMatchAnyDesc 65 | RbacRequireMatchAllDesc 66 | RbacDenyMatchAllDesc 67 | RbacDenyMatchAnyDesc 68 | ) 69 | 70 | const ( 71 | TransactionDesc Directive = iota + 10801 72 | TransactionArgMaxWaitSecondsDesc 73 | TransactionArgTimeoutSecondsDesc 74 | TransactionArgIsolationLevelDesc 75 | ) 76 | 77 | const ( 78 | TransformDesc Directive = iota + 10901 79 | TransformArgGetDesc 80 | ) 81 | 82 | const ( 83 | WhereInputDesc Directive = iota + 11001 84 | WhereInputFieldNotDesc 85 | WhereInputFieldFilterDesc 86 | WhereInputFilterFieldFieldDesc 87 | WhereInputFilterFieldScalarDesc 88 | WhereInputFilterFieldRelationDesc 89 | WhereInputFilterCommonFieldTypeDesc 90 | WhereInputScalarFilterFieldInsensitiveDesc 91 | WhereInputRelationFilterFieldWhereDesc 92 | ) 93 | 94 | const InjectRuleValueDesc Directive = iota + 11101 95 | 96 | const DisallowParallelDesc Directive = iota + 11201 97 | const CustomizedFieldDesc Directive = iota + 11301 98 | const SkipVariableDesc Directive = iota + 11401 99 | const AsyncResolveDesc Directive = iota + 11501 100 | const FirstRawResultDesc Directive = iota + 11601 101 | const ExportMatchDesc Directive = iota + 11701 102 | -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/operation_disallowparallel.toml: -------------------------------------------------------------------------------- 1 | DisallowParallelDesc = "作用于OPERATION上,禁止graphql并行解析" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/operation_internaloperation.toml: -------------------------------------------------------------------------------- 1 | InternalOperationDesc = "作用于OPERATION上,将其声明为内部函数,不对外暴露" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/operation_rbac.toml: -------------------------------------------------------------------------------- 1 | RbacDesc = "作用于OPERATION上,声明API的RBAC权限" 2 | RbacRequireMatchAnyDesc = "任意匹配,用户角色与API角色有交集时,可访问(常用)" 3 | RbacRequireMatchAllDesc = "全部匹配,用户角色包含API角色时,可访问" 4 | RbacDenyMatchAllDesc = "非全部匹配,当任意匹配或互斥匹配时,可访问" 5 | RbacDenyMatchAnyDesc = "互斥匹配,用户角色与API角色互斥时,可访问" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/operation_transaction.toml: -------------------------------------------------------------------------------- 1 | TransactionDesc = "作用于MUTATION OPERATION上,指定当前变更为事务操作" 2 | TransactionArgMaxWaitSecondsDesc = "等待时间" 3 | TransactionArgTimeoutSecondsDesc = "超时时间" 4 | TransactionArgIsolationLevelDesc = "隔离级别" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/selection_asyncresolve.toml: -------------------------------------------------------------------------------- 1 | AsyncResolveDesc = "作用于标量选择集上,并行解析数组提升响应速度" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/selection_customizedfield.toml: -------------------------------------------------------------------------------- 1 | CustomizedFieldDesc = "作用于标量选择集上,自定义字段,可以在钩子和返回值中看到" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/selection_export.toml: -------------------------------------------------------------------------------- 1 | ExportDesc = "作用于标量选择集上,将字段赋值给@internal声明的变量" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/selection_exportmatch.toml: -------------------------------------------------------------------------------- 1 | ExportMatchDesc = "作用于标量选择集上,匹配@export的变量用于解决N+1查询问题" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/selection_firstrawresult.toml: -------------------------------------------------------------------------------- 1 | FirstRawResultDesc = "作用于标量选择集上,用于提取首个 QueryRaw/ExecuteRaw 响应" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/selection_formatdatetime.toml: -------------------------------------------------------------------------------- 1 | FormatDateTimeDesc = "作用在字段上,用于格式化日期" 2 | FormatDateTimeArgFormatDesc = "枚举值,系统内置的标准格式,如 ISO8601" 3 | FormatDateTimeArgCustomFormatDesc = "自定义格式,需遵循Golang规范,例如 2006-01-02 15:04:05" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/selection_skipvariable.toml: -------------------------------------------------------------------------------- 1 | SkipVariableDesc = "作用于标量选择集上,根据条件跳过参数填充" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/selection_transform.toml: -------------------------------------------------------------------------------- 1 | TransformDesc = "作用于对象/数组类型的选择集上,将其拍扁" 2 | TransformArgGetDesc = "示例用法:info.name" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/variable_fromclaim.toml: -------------------------------------------------------------------------------- 1 | FromClaimDesc = "作用于变量上,用于注入用户信息" 2 | FromClaimArgNameDesc = "用于String变量,注入OIDC Claim对象声明的值,如USERID等" 3 | FromClaimArgCustomJsonPathDesc = "用于任意变量,name=CUSTOM时生效,以数组形式指定json path,从CustomClaims中提取数据" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/variable_fromheader.toml: -------------------------------------------------------------------------------- 1 | FromHeaderDesc = "作用于String变量上,用于注入请求头中的字段" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/variable_hookvariable.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fireboomio/fireboom/23d731b087109ed6281fc83eaff225fb82723755/pkg/plugins/i18n/directive/zh_cn/variable_hookvariable.toml -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/variable_injectcurrentdatetime.toml: -------------------------------------------------------------------------------- 1 | InjectCurrentDateTimeDesc = "作用于String变量上,用于注入当前时间" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/variable_injectenvironmentvariable.toml: -------------------------------------------------------------------------------- 1 | InjectEnvironmentVariableDesc = "作用于String变量上,用于注入环境变量" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/variable_injectgenerateduuid.toml: -------------------------------------------------------------------------------- 1 | InjectGeneratedUUIDDesc = "作用于String变量上,用于注入UUID" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/variable_injectruleValue.toml: -------------------------------------------------------------------------------- 1 | InjectRuleValueDesc = "作用于变量上,根据表达式注入参数,可以从arguments,request.header, request.body, environment获取参数" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/variable_internal.toml: -------------------------------------------------------------------------------- 1 | InternalDesc = "作用于变量上,用于声明变量,和_join和export一起使用" -------------------------------------------------------------------------------- /pkg/plugins/i18n/directive/zh_cn/variable_jsonschema.toml: -------------------------------------------------------------------------------- 1 | JsonschemaDesc = "作用于变量上,用于入参校验" 2 | JsonschemaArgMinimumDesc = "用于数字类型变量,变量>minimum" 3 | JsonschemaArgMaximumDesc = "用于数字类型变量,变量 release/build_time 5 | export FB_COMMIT=$(git rev-parse --short HEAD) 6 | 7 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w -X main.FbVersion=$FB_VERSION -X main.FbCommit=$FB_COMMIT" -o release/fireboom-mac$BIN_SUFFIX app/main.go 8 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w -X main.FbVersion=$FB_VERSION -X main.FbCommit=$FB_COMMIT" -o release/fireboom-mac-arm64$BIN_SUFFIX app/main.go 9 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.FbVersion=$FB_VERSION -X main.FbCommit=$FB_COMMIT" -o release/fireboom-linux$BIN_SUFFIX app/main.go 10 | CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X main.FbVersion=$FB_VERSION -X main.FbCommit=$FB_COMMIT" -o release/fireboom-linux-arm64$BIN_SUFFIX app/main.go 11 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.FbVersion=$FB_VERSION -X main.FbCommit=$FB_COMMIT" -o release/fireboom-windows$BIN_SUFFIX.exe app/main.go 12 | CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags "-s -w -X main.FbVersion=$FB_VERSION -X main.FbCommit=$FB_COMMIT" -o release/fireboom-windows-arm64$BIN_SUFFIX.exe app/main.go -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | export FB_COMMIT=$(git rev-parse --short HEAD) 3 | go build -ldflags "-s -w -X main.FbVersion=$FB_VERSION -X main.FbCommit=$FB_COMMIT" -o release/fireboom$BIN_SUFFIX app/main.go -------------------------------------------------------------------------------- /scripts/clear-bin.sh: -------------------------------------------------------------------------------- 1 | rm -f release/fireboom-mac$BIN_SUFFIX 2 | rm -f release/fireboom-mac-arm64$BIN_SUFFIX 3 | rm -f release/fireboom-linux$BIN_SUFFIX 4 | rm -f release/fireboom-linux-arm64$BIN_SUFFIX 5 | rm -f release/fireboom-windows$BIN_SUFFIX.exe 6 | rm -f release/fireboom-windows-arm64$BIN_SUFFIX.exe -------------------------------------------------------------------------------- /scripts/export-commit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | logDir=../merged-log 4 | if [ ! -d $logDir ]; then 5 | mkdir "$logDir" 6 | fi 7 | logFilePath=$logDir/$(date "+%Y-%m-%d").txt 8 | cd ../wundergraphGitSubmodule || exit 9 | echo "commit hash:$(git rev-parse HEAD)" > "$logFilePath" 10 | # shellcheck disable=SC2129 11 | echo "commit tag:$(git describe --tags --abbrev=0)" >> "$logFilePath" 12 | echo "commit message:" >> "$logFilePath" 13 | git log -1 --pretty=%B >> "$logFilePath" 14 | -------------------------------------------------------------------------------- /scripts/pull-submodule.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cur_dir=$( 4 | cd "$(dirname "$0")" || exit 5 | pwd 6 | ) 7 | 8 | function batch_cmd() { 9 | path=$cur_dir/$1 10 | branch=$2 11 | echo "prepare git pull from branch [$branch] for module [$path]" 12 | cd "$path" && 13 | git fetch && 14 | git checkout "$branch" && 15 | git pull 16 | } 17 | 18 | batch_cmd "../assets/front" "release" 19 | batch_cmd "../wundergraphGitSubmodule" "main" -------------------------------------------------------------------------------- /scripts/skip-front.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | INSERT_CHAR="/" 3 | ASSET_FILEPATH="assets/asset.go" 4 | if [ "$(uname -s)" = "Darwin" ];then 5 | sed -i '' "10s|^|${INSERT_CHAR}|" "$ASSET_FILEPATH" 6 | sed -i '' "13s|^|${INSERT_CHAR}|" "$ASSET_FILEPATH" 7 | else 8 | sed -i "10s|^|${INSERT_CHAR}|" "$ASSET_FILEPATH" 9 | sed -i "13s|^|${INSERT_CHAR}|" "$ASSET_FILEPATH" 10 | fi 11 | -------------------------------------------------------------------------------- /scripts/tag-all.sh: -------------------------------------------------------------------------------- 1 | mkdir -p release/versions/$FB_VERSION 2 | cp -r release/fireboom*.tar.gz release/versions/$FB_VERSION/ -------------------------------------------------------------------------------- /scripts/tar-all.sh: -------------------------------------------------------------------------------- 1 | tar -zcvf release/fireboom-windows$BIN_SUFFIX.exe.tar.gz -C release fireboom-windows$BIN_SUFFIX.exe 2 | tar -zcvf release/fireboom-windows-arm64$BIN_SUFFIX.exe.tar.gz -C release fireboom-windows-arm64$BIN_SUFFIX.exe 3 | tar -zcvf release/fireboom-mac$BIN_SUFFIX.tar.gz -C release fireboom-mac$BIN_SUFFIX 4 | tar -zcvf release/fireboom-mac-arm64$BIN_SUFFIX.tar.gz -C release fireboom-mac-arm64$BIN_SUFFIX 5 | tar -zcvf release/fireboom-linux$BIN_SUFFIX.tar.gz -C release fireboom-linux$BIN_SUFFIX 6 | tar -zcvf release/fireboom-linux-arm64$BIN_SUFFIX.tar.gz -C release fireboom-linux-arm64$BIN_SUFFIX --------------------------------------------------------------------------------