Skip to content

Commit

Permalink
FEAT: Adding GraphQL Service Generation (#28)
Browse files Browse the repository at this point in the history
* feat: adding graphql service for accounts

* feat: adding support for accounts-graphql schema

* fix: scale machines to 0

* fix: fly.toml

* fix: internal port 8080 in production

* fix: change back to port 3000

* fix: move graph into internal

* feat: code generation for gql

* fix: models

* fix: models

* feat: resolvers are fixed

* feat: updating resolver path

* feat: move schemas into their own directory

* fix: moving models to a package

* feat: adding proper generated files package

* feat: adding apollo router locally for dev

* feat: logged todos resolver is called

* fix: empty resolvers

* fix: moving schema configs to dev

* fix: moving schema configs to dev

* feat: adding apolo router

* feat: super graph dev and prod setup

* feat: remove the custom metrics

* fix: deployment for apollo router

* fix: delete older files

* feat: updating dockerfile for router

* fix: remove homepage

* fix: disable homepage

* fix: disable homepage

* fix: router config

* fix: config

* fix: apollo router config

* fix: dockerfile

* fix: dockerfile slashes

* fix: apollo router prod

* fix: prod

* fix: http service for accounts graphql

* fix: revert back to public prod url

* fix: graphql schema

* feat: account model generation

* feat: update prod graph

* fix: use internal routing

* feat: adding accounts internal

* feat: hide accounts-graphql behind VPC

* feat: graphql service generator

* feat: cleanup graphql files

* fix: file generation

* fix: further cleanup

* fix: remove unwanted package in go workspace
  • Loading branch information
ericzorn93 authored Jan 2, 2025
1 parent c742bc9 commit 37d78bc
Show file tree
Hide file tree
Showing 36 changed files with 4,556 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('service-gen generator', () => {
let tree: Tree;
const options: CreateBackendServiceGeneratorSchema = {
serviceName: 'test',
serviceType: 'app',
serviceType: 'api',
};

beforeEach(() => {
Expand Down
32 changes: 29 additions & 3 deletions tools/backend-service/src/generators/service-gen/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
addProjectConfiguration,
formatFiles,
generateFiles,
ProjectConfiguration,
Tree,
} from '@nx/devkit';
import * as path from 'path';
Expand All @@ -13,7 +14,8 @@ export async function createBackendServiceGenerator(
options: CreateBackendServiceGeneratorSchema
) {
const projectRoot = `apps/services/${options.serviceName}`;
addProjectConfiguration(tree, options.serviceName, {

const defaultConfig: ProjectConfiguration = {
root: projectRoot,
projectType: 'application',
sourceRoot: projectRoot,
Expand Down Expand Up @@ -58,8 +60,32 @@ export async function createBackendServiceGenerator(
},
},
},
});
generateFiles(tree, path.join(__dirname, 'files'), projectRoot, options);
};

let finalConfig: ProjectConfiguration = defaultConfig;
let filesPath: string = path.join(__dirname, 'api-worker-files');

switch (options.serviceType) {
case 'graphql':
Object.assign(finalConfig.targets, {
generate: {
executor: 'nx:run-commands',
options: {
cwd: '{projectRoot}',
commands: ['go generate ./...', 'pnpm graphql:gen:dev'],
},
},
});
filesPath = path.join(__dirname, 'graphql-files');
break;
case 'api':
case 'worker':
default:
break;
}

addProjectConfiguration(tree, options.serviceName, finalConfig);
generateFiles(tree, filesPath, projectRoot, options);
await formatFiles(tree);
await execSync('pnpm go:tidy');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
AMQP_CONNECTION_URI=""
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.work .
COPY go.work.sum .
COPY ./libs ./libs
COPY ./apps/services ./apps/services
ENV GOOS=linux
ENV GOARCH=amd64
ENV CGO_ENABLED=0
RUN go build -o /bin/<%=serviceName %> ./apps/services/<%=serviceName %>/cmd/server/main.go

FROM alpine:3.20
RUN apk update --no-cache bash curl
WORKDIR /app
COPY --from=builder /bin/<%=serviceName %> /bin/<%=serviceName %>
EXPOSE 3000
CMD [ "/bin/<%=serviceName %>" ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package main

import (
"apps/services/<%= serviceName %>/internal/config"
"apps/services/<%= serviceName %>/internal/graph/generated"
"apps/services/<%= serviceName %>/internal/graph/resolvers"
"context"
"errors"
"log"
"log/slog"
"os"

"connectrpc.com/connect"
"connectrpc.com/validate"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/handler/extension"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/99designs/gqlgen/graphql/playground"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"

"libs/backend/boot"
)

// serviceName is the name of the microservice
const serviceName = "<%= serviceName %>"

func run() error {
// Application Context
ctx := context.Background()

// Create logger
logger := boot.NewSlogger()

// Construct config
config, err := config.NewConfig()
if err != nil {
logger.Error("Trouble constructing config")
os.Exit(1)
}

// Connect Interceptors
validationInterceptor, err := validate.NewInterceptor()
if err != nil {
logger.Error("Cannot set up validation interceptor", slog.Any("error", err))
return err
}
_ = []connect.HandlerOption{
connect.WithInterceptors(validationInterceptor),
}

// Initialize the gRPC Options
bootService := boot.
NewBuildServiceBuilder().
SetServiceName(serviceName).
SetLogger(logger).
SetAMQPOptions(boot.AMQPOptions{
ConnectionURI: config.AMQPUrl,
OnConnectionCallback: func(params boot.AMQPCallBackParams) error {
params.Logger.Info("AMQP connected successfully")

// Set up all AMQP queues and exchanges

params.Logger.Info("Set up all AMQP queues and exchanges")

return nil
},
}).
SetConnectRPCOptions(boot.ConnectRPCOptions{
Port: 3000,
TransportCredentials: []credentials.TransportCredentials{
insecure.NewCredentials(),
},
Handlers: []boot.ConnectRPCHandler{
func(params boot.ConnectRPCHandlerParams) error {
if !params.AMQPController.IsConnected() {
errMsg := "AMQP not conntected"
logger.Error(errMsg)
return errors.New(errMsg)
}

// Set up all ConnectRPC Handlers
srv := handler.New(generated.NewExecutableSchema(generated.Config{Resolvers: &resolvers.Resolver{}}))
srv.AddTransport(transport.Options{})
srv.AddTransport(transport.GET{})
srv.AddTransport(transport.POST{})
// srv.SetQueryCache(lru.New[*ast.QueryDocument](1000))

// Middleware
srv.Use(extension.Introspection{})
// srv.Use(extension.AutomaticPersistedQuery{
// Cache: lru.New[string](100),
// })

params.Mux.Handle("/", playground.Handler("GraphQL playground", "/graphql"))
params.Mux.Handle("/graphql", srv)

return nil
},
},
}).
SetBootCallbacks([]boot.BootCallback{
func(params boot.BootCallbackParams) error {
params.Logger.Info("Service booted successfully", slog.String("serviceName", serviceName))
return nil
},
}).
Build()

return bootService.Start(ctx)
}

func main() {
if err := run(); err != nil {
log.Printf("Cannot start service %s", serviceName)
os.Exit(1)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# fly.toml app configuration file generated for accounts-graphql-prod on 2024-12-31T14:23:36-05:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = '<%= serviceName %>-prod'
primary_region = 'ewr'

[build]
dockerfile = 'Dockerfile'
ignorefile = '.dockerignore'

[[vm]]
memory = '1gb'
cpu_kind = 'shared'
cpus = 1

[[metrics]]
port = 3000
path = '/metrics'
https = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module apps/services/<%=serviceName %>

go 1.23

require (
connectrpc.com/connect v1.17.0
connectrpc.com/validate v0.1.0
github.com/99designs/gqlgen v0.17.61
github.com/vektah/gqlparser/v2 v2.5.20
google.golang.org/grpc v1.68.1
)

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1 // indirect
cel.dev/expr v0.18.0 // indirect
github.com/agnivade/levenshtein v1.2.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/bufbuild/protovalidate-go v0.8.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/google/cel-go v0.22.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/protobuf v1.35.2 // indirect
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1 h1:jLd96rDDNJ+zIJxvV/L855VEtrjR0G4aePVDlCpf6kw=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1/go.mod h1:mnHCFccv4HwuIAOHNGdiIc5ZYbBCvbTWZcodLN5wITI=
cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo=
cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk=
connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
connectrpc.com/validate v0.1.0 h1:r55jirxMK7HO/xZwVHj3w2XkVFarsUM77ZDy367NtH4=
connectrpc.com/validate v0.1.0/go.mod h1:GU47c9/x/gd+u9wRSPkrQOP46gx2rMN+Wo37EHgI3Ow=
github.com/99designs/gqlgen v0.17.61 h1:vE7xLRC066n9wehgjeplILOWtwz75zbzcV2/Iv9i3pw=
github.com/99designs/gqlgen v0.17.61/go.mod h1:rFU1T3lhv/tPeAlww/DJ4ol2YxT/pPpue+xxPbkd3r4=
github.com/PuerkitoBio/goquery v1.9.3 h1:mpJr/ikUA9/GNJB/DBZcGeFDXUtosHRyRrwh7KGdTG0=
github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G6D4LCYA6u4U=
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/bufbuild/protovalidate-go v0.8.0 h1:Xs3kCLCJ4tQiogJ0iOXm+ClKw/KviW3nLAryCGW2I3Y=
github.com/bufbuild/protovalidate-go v0.8.0/go.mod h1:JPWZInGm2y2NBg3vKDKdDIkvDjyLv31J3hLH5GIFc/Q=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/cel-go v0.22.1 h1:AfVXx3chM2qwoSbM7Da8g8hX8OVSkBFwX+rz2+PcK40=
github.com/google/cel-go v0.22.1/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vektah/gqlparser/v2 v2.5.20 h1:kPaWbhBntxoZPaNdBaIPT1Kh0i1b/onb5kXgEdP5JCo=
github.com/vektah/gqlparser/v2 v2.5.20/go.mod h1:xMl+ta8a5M1Yo1A1Iwt/k7gSpscwSnHZdw7tfhEGfTM=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading

0 comments on commit 37d78bc

Please sign in to comment.