create-svc 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/package.json +4 -1
  2. package/src/cli.ts +328 -108
  3. package/src/gcp.test.ts +71 -0
  4. package/src/gcp.ts +97 -0
  5. package/src/naming.test.ts +37 -0
  6. package/src/naming.ts +103 -0
  7. package/src/neon.test.ts +48 -0
  8. package/src/neon.ts +76 -0
  9. package/src/post-scaffold.ts +77 -0
  10. package/src/scaffold.test.ts +66 -31
  11. package/src/scaffold.ts +60 -55
  12. package/templates/shared/.github/workflows/ci.yml +22 -0
  13. package/templates/shared/.github/workflows/deploy.yml +30 -0
  14. package/templates/shared/.github/workflows/personal.yml +41 -0
  15. package/templates/shared/.github/workflows/preview-cleanup.yml +25 -0
  16. package/templates/shared/.github/workflows/preview.yml +29 -0
  17. package/templates/shared/README.md +37 -0
  18. package/templates/shared/scripts/cloudrun/bootstrap.ts +76 -0
  19. package/templates/shared/scripts/cloudrun/config.ts +57 -0
  20. package/templates/shared/scripts/cloudrun/deploy.ts +82 -0
  21. package/templates/shared/scripts/cloudrun/lib.ts +380 -0
  22. package/templates/shared/scripts/cloudrun/neon.ts +104 -0
  23. package/templates/shared/service.yaml +28 -0
  24. package/templates/variants/bun-connectrpc/Dockerfile +13 -0
  25. package/templates/variants/bun-connectrpc/package.json +20 -0
  26. package/templates/variants/bun-connectrpc/scripts/codegen.ts +1 -0
  27. package/templates/variants/bun-connectrpc/src/index.ts +32 -0
  28. package/templates/variants/bun-connectrpc/test/app.test.ts +17 -0
  29. package/templates/variants/bun-connectrpc/tsconfig.json +10 -0
  30. package/templates/variants/bun-hono/Dockerfile +13 -0
  31. package/templates/variants/bun-hono/package.json +21 -0
  32. package/templates/variants/bun-hono/scripts/codegen.ts +1 -0
  33. package/templates/variants/bun-hono/src/index.ts +24 -0
  34. package/templates/variants/bun-hono/test/app.test.ts +12 -0
  35. package/templates/variants/bun-hono/tsconfig.json +10 -0
  36. package/templates/variants/go-chi/Dockerfile +23 -0
  37. package/templates/variants/go-chi/buf.gen.yaml +10 -0
  38. package/templates/variants/go-chi/buf.yaml +9 -0
  39. package/templates/variants/go-chi/cmd/server/main.go +52 -0
  40. package/templates/variants/go-chi/gen/dns/v1/dns.pb.go +623 -0
  41. package/templates/variants/go-chi/gen/dns/v1/dnsv1connect/dns.connect.go +192 -0
  42. package/templates/variants/go-chi/go.mod +10 -0
  43. package/templates/variants/go-chi/internal/app/service.go +109 -0
  44. package/templates/variants/go-chi/internal/app/token_source.go +50 -0
  45. package/templates/variants/go-chi/internal/cloudflare/client.go +160 -0
  46. package/templates/variants/go-chi/internal/config/config.go +23 -0
  47. package/templates/variants/go-chi/internal/connectapi/handler.go +79 -0
  48. package/templates/variants/go-chi/internal/httpapi/routes.go +93 -0
  49. package/templates/variants/go-chi/internal/vault/client.go +148 -0
  50. package/templates/variants/go-chi/package.json +16 -0
  51. package/templates/variants/go-chi/protos/dns/v1/dns.proto +58 -0
  52. package/templates/variants/go-chi/test/go.test.ts +19 -0
  53. package/templates/variants/go-connectrpc/Dockerfile +23 -0
  54. package/templates/variants/go-connectrpc/buf.gen.yaml +10 -0
  55. package/templates/variants/go-connectrpc/buf.yaml +9 -0
  56. package/templates/variants/go-connectrpc/cmd/server/main.go +51 -0
  57. package/templates/variants/go-connectrpc/gen/dns/v1/dns.pb.go +623 -0
  58. package/templates/variants/go-connectrpc/gen/dns/v1/dnsv1connect/dns.connect.go +192 -0
  59. package/templates/variants/go-connectrpc/go.mod +10 -0
  60. package/templates/variants/go-connectrpc/internal/app/service.go +109 -0
  61. package/templates/variants/go-connectrpc/internal/app/token_source.go +50 -0
  62. package/templates/variants/go-connectrpc/internal/cloudflare/client.go +160 -0
  63. package/templates/variants/go-connectrpc/internal/config/config.go +23 -0
  64. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +79 -0
  65. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +93 -0
  66. package/templates/variants/go-connectrpc/internal/vault/client.go +148 -0
  67. package/templates/variants/go-connectrpc/package.json +16 -0
  68. package/templates/variants/go-connectrpc/protos/dns/v1/dns.proto +58 -0
  69. package/templates/variants/go-connectrpc/test/go.test.ts +19 -0
@@ -0,0 +1,192 @@
1
+ // Code generated by protoc-gen-connect-go. DO NOT EDIT.
2
+ //
3
+ // Source: dns/v1/dns.proto
4
+
5
+ package dnsv1connect
6
+
7
+ import (
8
+ connect "connectrpc.com/connect"
9
+ context "context"
10
+ errors "errors"
11
+ v1 "{{MODULE_PATH}}/gen/dns/v1"
12
+ http "net/http"
13
+ strings "strings"
14
+ )
15
+
16
+ // This is a compile-time assertion to ensure that this generated file and the connect package are
17
+ // compatible. If you get a compiler error that this constant is not defined, this code was
18
+ // generated with a version of connect newer than the one compiled into your binary. You can fix the
19
+ // problem by either regenerating this code with an older version of connect or updating the connect
20
+ // version compiled into your binary.
21
+ const _ = connect.IsAtLeastVersion1_13_0
22
+
23
+ const (
24
+ // DNSServiceName is the fully-qualified name of the DNSService service.
25
+ DNSServiceName = "dns.v1.DNSService"
26
+ )
27
+
28
+ // These constants are the fully-qualified names of the RPCs defined in this package. They're
29
+ // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
30
+ //
31
+ // Note that these are different from the fully-qualified method names used by
32
+ // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
33
+ // reflection-formatted method names, remove the leading slash and convert the remaining slash to a
34
+ // period.
35
+ const (
36
+ // DNSServiceListRecordsProcedure is the fully-qualified name of the DNSService's ListRecords RPC.
37
+ DNSServiceListRecordsProcedure = "/dns.v1.DNSService/ListRecords"
38
+ // DNSServiceCreateRecordProcedure is the fully-qualified name of the DNSService's CreateRecord RPC.
39
+ DNSServiceCreateRecordProcedure = "/dns.v1.DNSService/CreateRecord"
40
+ // DNSServiceUpdateRecordProcedure is the fully-qualified name of the DNSService's UpdateRecord RPC.
41
+ DNSServiceUpdateRecordProcedure = "/dns.v1.DNSService/UpdateRecord"
42
+ // DNSServiceDeleteRecordProcedure is the fully-qualified name of the DNSService's DeleteRecord RPC.
43
+ DNSServiceDeleteRecordProcedure = "/dns.v1.DNSService/DeleteRecord"
44
+ )
45
+
46
+ // DNSServiceClient is a client for the dns.v1.DNSService service.
47
+ type DNSServiceClient interface {
48
+ ListRecords(context.Context, *connect.Request[v1.ListRecordsRequest]) (*connect.Response[v1.ListRecordsResponse], error)
49
+ CreateRecord(context.Context, *connect.Request[v1.CreateRecordRequest]) (*connect.Response[v1.CreateRecordResponse], error)
50
+ UpdateRecord(context.Context, *connect.Request[v1.UpdateRecordRequest]) (*connect.Response[v1.UpdateRecordResponse], error)
51
+ DeleteRecord(context.Context, *connect.Request[v1.DeleteRecordRequest]) (*connect.Response[v1.DeleteRecordResponse], error)
52
+ }
53
+
54
+ // NewDNSServiceClient constructs a client for the dns.v1.DNSService service. By default, it uses
55
+ // the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends
56
+ // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or
57
+ // connect.WithGRPCWeb() options.
58
+ //
59
+ // The URL supplied here should be the base URL for the Connect or gRPC server (for example,
60
+ // http://api.acme.com or https://acme.com/grpc).
61
+ func NewDNSServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) DNSServiceClient {
62
+ baseURL = strings.TrimRight(baseURL, "/")
63
+ dNSServiceMethods := v1.File_dns_v1_dns_proto.Services().ByName("DNSService").Methods()
64
+ return &dNSServiceClient{
65
+ listRecords: connect.NewClient[v1.ListRecordsRequest, v1.ListRecordsResponse](
66
+ httpClient,
67
+ baseURL+DNSServiceListRecordsProcedure,
68
+ connect.WithSchema(dNSServiceMethods.ByName("ListRecords")),
69
+ connect.WithClientOptions(opts...),
70
+ ),
71
+ createRecord: connect.NewClient[v1.CreateRecordRequest, v1.CreateRecordResponse](
72
+ httpClient,
73
+ baseURL+DNSServiceCreateRecordProcedure,
74
+ connect.WithSchema(dNSServiceMethods.ByName("CreateRecord")),
75
+ connect.WithClientOptions(opts...),
76
+ ),
77
+ updateRecord: connect.NewClient[v1.UpdateRecordRequest, v1.UpdateRecordResponse](
78
+ httpClient,
79
+ baseURL+DNSServiceUpdateRecordProcedure,
80
+ connect.WithSchema(dNSServiceMethods.ByName("UpdateRecord")),
81
+ connect.WithClientOptions(opts...),
82
+ ),
83
+ deleteRecord: connect.NewClient[v1.DeleteRecordRequest, v1.DeleteRecordResponse](
84
+ httpClient,
85
+ baseURL+DNSServiceDeleteRecordProcedure,
86
+ connect.WithSchema(dNSServiceMethods.ByName("DeleteRecord")),
87
+ connect.WithClientOptions(opts...),
88
+ ),
89
+ }
90
+ }
91
+
92
+ // dNSServiceClient implements DNSServiceClient.
93
+ type dNSServiceClient struct {
94
+ listRecords *connect.Client[v1.ListRecordsRequest, v1.ListRecordsResponse]
95
+ createRecord *connect.Client[v1.CreateRecordRequest, v1.CreateRecordResponse]
96
+ updateRecord *connect.Client[v1.UpdateRecordRequest, v1.UpdateRecordResponse]
97
+ deleteRecord *connect.Client[v1.DeleteRecordRequest, v1.DeleteRecordResponse]
98
+ }
99
+
100
+ // ListRecords calls dns.v1.DNSService.ListRecords.
101
+ func (c *dNSServiceClient) ListRecords(ctx context.Context, req *connect.Request[v1.ListRecordsRequest]) (*connect.Response[v1.ListRecordsResponse], error) {
102
+ return c.listRecords.CallUnary(ctx, req)
103
+ }
104
+
105
+ // CreateRecord calls dns.v1.DNSService.CreateRecord.
106
+ func (c *dNSServiceClient) CreateRecord(ctx context.Context, req *connect.Request[v1.CreateRecordRequest]) (*connect.Response[v1.CreateRecordResponse], error) {
107
+ return c.createRecord.CallUnary(ctx, req)
108
+ }
109
+
110
+ // UpdateRecord calls dns.v1.DNSService.UpdateRecord.
111
+ func (c *dNSServiceClient) UpdateRecord(ctx context.Context, req *connect.Request[v1.UpdateRecordRequest]) (*connect.Response[v1.UpdateRecordResponse], error) {
112
+ return c.updateRecord.CallUnary(ctx, req)
113
+ }
114
+
115
+ // DeleteRecord calls dns.v1.DNSService.DeleteRecord.
116
+ func (c *dNSServiceClient) DeleteRecord(ctx context.Context, req *connect.Request[v1.DeleteRecordRequest]) (*connect.Response[v1.DeleteRecordResponse], error) {
117
+ return c.deleteRecord.CallUnary(ctx, req)
118
+ }
119
+
120
+ // DNSServiceHandler is an implementation of the dns.v1.DNSService service.
121
+ type DNSServiceHandler interface {
122
+ ListRecords(context.Context, *connect.Request[v1.ListRecordsRequest]) (*connect.Response[v1.ListRecordsResponse], error)
123
+ CreateRecord(context.Context, *connect.Request[v1.CreateRecordRequest]) (*connect.Response[v1.CreateRecordResponse], error)
124
+ UpdateRecord(context.Context, *connect.Request[v1.UpdateRecordRequest]) (*connect.Response[v1.UpdateRecordResponse], error)
125
+ DeleteRecord(context.Context, *connect.Request[v1.DeleteRecordRequest]) (*connect.Response[v1.DeleteRecordResponse], error)
126
+ }
127
+
128
+ // NewDNSServiceHandler builds an HTTP handler from the service implementation. It returns the path
129
+ // on which to mount the handler and the handler itself.
130
+ //
131
+ // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
132
+ // and JSON codecs. They also support gzip compression.
133
+ func NewDNSServiceHandler(svc DNSServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
134
+ dNSServiceMethods := v1.File_dns_v1_dns_proto.Services().ByName("DNSService").Methods()
135
+ dNSServiceListRecordsHandler := connect.NewUnaryHandler(
136
+ DNSServiceListRecordsProcedure,
137
+ svc.ListRecords,
138
+ connect.WithSchema(dNSServiceMethods.ByName("ListRecords")),
139
+ connect.WithHandlerOptions(opts...),
140
+ )
141
+ dNSServiceCreateRecordHandler := connect.NewUnaryHandler(
142
+ DNSServiceCreateRecordProcedure,
143
+ svc.CreateRecord,
144
+ connect.WithSchema(dNSServiceMethods.ByName("CreateRecord")),
145
+ connect.WithHandlerOptions(opts...),
146
+ )
147
+ dNSServiceUpdateRecordHandler := connect.NewUnaryHandler(
148
+ DNSServiceUpdateRecordProcedure,
149
+ svc.UpdateRecord,
150
+ connect.WithSchema(dNSServiceMethods.ByName("UpdateRecord")),
151
+ connect.WithHandlerOptions(opts...),
152
+ )
153
+ dNSServiceDeleteRecordHandler := connect.NewUnaryHandler(
154
+ DNSServiceDeleteRecordProcedure,
155
+ svc.DeleteRecord,
156
+ connect.WithSchema(dNSServiceMethods.ByName("DeleteRecord")),
157
+ connect.WithHandlerOptions(opts...),
158
+ )
159
+ return "/dns.v1.DNSService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
160
+ switch r.URL.Path {
161
+ case DNSServiceListRecordsProcedure:
162
+ dNSServiceListRecordsHandler.ServeHTTP(w, r)
163
+ case DNSServiceCreateRecordProcedure:
164
+ dNSServiceCreateRecordHandler.ServeHTTP(w, r)
165
+ case DNSServiceUpdateRecordProcedure:
166
+ dNSServiceUpdateRecordHandler.ServeHTTP(w, r)
167
+ case DNSServiceDeleteRecordProcedure:
168
+ dNSServiceDeleteRecordHandler.ServeHTTP(w, r)
169
+ default:
170
+ http.NotFound(w, r)
171
+ }
172
+ })
173
+ }
174
+
175
+ // UnimplementedDNSServiceHandler returns CodeUnimplemented from all methods.
176
+ type UnimplementedDNSServiceHandler struct{}
177
+
178
+ func (UnimplementedDNSServiceHandler) ListRecords(context.Context, *connect.Request[v1.ListRecordsRequest]) (*connect.Response[v1.ListRecordsResponse], error) {
179
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("dns.v1.DNSService.ListRecords is not implemented"))
180
+ }
181
+
182
+ func (UnimplementedDNSServiceHandler) CreateRecord(context.Context, *connect.Request[v1.CreateRecordRequest]) (*connect.Response[v1.CreateRecordResponse], error) {
183
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("dns.v1.DNSService.CreateRecord is not implemented"))
184
+ }
185
+
186
+ func (UnimplementedDNSServiceHandler) UpdateRecord(context.Context, *connect.Request[v1.UpdateRecordRequest]) (*connect.Response[v1.UpdateRecordResponse], error) {
187
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("dns.v1.DNSService.UpdateRecord is not implemented"))
188
+ }
189
+
190
+ func (UnimplementedDNSServiceHandler) DeleteRecord(context.Context, *connect.Request[v1.DeleteRecordRequest]) (*connect.Response[v1.DeleteRecordResponse], error) {
191
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("dns.v1.DNSService.DeleteRecord is not implemented"))
192
+ }
@@ -0,0 +1,10 @@
1
+ module {{MODULE_PATH}}
2
+
3
+ go 1.25.4
4
+
5
+ require (
6
+ connectrpc.com/connect v1.19.1
7
+ github.com/go-chi/chi/v5 v5.2.2
8
+ golang.org/x/net v0.42.0
9
+ google.golang.org/protobuf v1.36.10
10
+ )
@@ -0,0 +1,109 @@
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "sync"
7
+ )
8
+
9
+ type Record struct {
10
+ ID string `json:"id"`
11
+ Type string `json:"type"`
12
+ Name string `json:"name"`
13
+ Content string `json:"content"`
14
+ TTL int `json:"ttl"`
15
+ Proxied bool `json:"proxied"`
16
+ }
17
+
18
+ type CreateRecordInput struct {
19
+ Type string `json:"type"`
20
+ Name string `json:"name"`
21
+ Content string `json:"content"`
22
+ TTL int `json:"ttl"`
23
+ Proxied bool `json:"proxied"`
24
+ }
25
+
26
+ type UpdateRecordInput struct {
27
+ Type string `json:"type"`
28
+ Name string `json:"name"`
29
+ Content string `json:"content"`
30
+ TTL int `json:"ttl"`
31
+ Proxied bool `json:"proxied"`
32
+ }
33
+
34
+ type DNSService struct {
35
+ mu sync.RWMutex
36
+ nextID int
37
+ records map[string]Record
38
+ }
39
+
40
+ func NewDNSService() *DNSService {
41
+ return &DNSService{
42
+ nextID: 1,
43
+ records: map[string]Record{},
44
+ }
45
+ }
46
+
47
+ func (s *DNSService) ListRecords(ctx context.Context) ([]Record, error) {
48
+ _ = ctx
49
+ s.mu.RLock()
50
+ defer s.mu.RUnlock()
51
+
52
+ out := make([]Record, 0, len(s.records))
53
+ for _, record := range s.records {
54
+ out = append(out, record)
55
+ }
56
+ return out, nil
57
+ }
58
+
59
+ func (s *DNSService) CreateRecord(ctx context.Context, input CreateRecordInput) (Record, error) {
60
+ _ = ctx
61
+ s.mu.Lock()
62
+ defer s.mu.Unlock()
63
+
64
+ record := Record{
65
+ ID: fmt.Sprintf("%d", s.nextID),
66
+ Type: input.Type,
67
+ Name: input.Name,
68
+ Content: input.Content,
69
+ TTL: input.TTL,
70
+ Proxied: input.Proxied,
71
+ }
72
+
73
+ s.nextID += 1
74
+ s.records[record.ID] = record
75
+ return record, nil
76
+ }
77
+
78
+ func (s *DNSService) UpdateRecord(ctx context.Context, id string, input UpdateRecordInput) (Record, error) {
79
+ _ = ctx
80
+ s.mu.Lock()
81
+ defer s.mu.Unlock()
82
+
83
+ record, ok := s.records[id]
84
+ if !ok {
85
+ return Record{}, fmt.Errorf("record %s not found", id)
86
+ }
87
+
88
+ record.Type = input.Type
89
+ record.Name = input.Name
90
+ record.Content = input.Content
91
+ record.TTL = input.TTL
92
+ record.Proxied = input.Proxied
93
+ s.records[id] = record
94
+
95
+ return record, nil
96
+ }
97
+
98
+ func (s *DNSService) DeleteRecord(ctx context.Context, id string) error {
99
+ _ = ctx
100
+ s.mu.Lock()
101
+ defer s.mu.Unlock()
102
+
103
+ if _, ok := s.records[id]; !ok {
104
+ return fmt.Errorf("record %s not found", id)
105
+ }
106
+
107
+ delete(s.records, id)
108
+ return nil
109
+ }
@@ -0,0 +1,50 @@
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+ "os"
6
+ "strings"
7
+ "sync"
8
+ )
9
+
10
+ type SecretResolver interface {
11
+ Get(ctx context.Context, path string, key string) (string, error)
12
+ }
13
+
14
+ type CloudflareTokenSource struct {
15
+ resolver SecretResolver
16
+ secretPath string
17
+ secretKey string
18
+
19
+ mu sync.Mutex
20
+ cached string
21
+ }
22
+
23
+ func NewCloudflareTokenSource(resolver SecretResolver, secretPath string, secretKey string) *CloudflareTokenSource {
24
+ return &CloudflareTokenSource{
25
+ resolver: resolver,
26
+ secretPath: secretPath,
27
+ secretKey: secretKey,
28
+ }
29
+ }
30
+
31
+ func (s *CloudflareTokenSource) Token(ctx context.Context) (string, error) {
32
+ if token := strings.TrimSpace(os.Getenv("CLOUDFLARE_API_TOKEN")); token != "" {
33
+ return token, nil
34
+ }
35
+
36
+ s.mu.Lock()
37
+ defer s.mu.Unlock()
38
+
39
+ if s.cached != "" {
40
+ return s.cached, nil
41
+ }
42
+
43
+ token, err := s.resolver.Get(ctx, s.secretPath, s.secretKey)
44
+ if err != nil {
45
+ return "", err
46
+ }
47
+
48
+ s.cached = token
49
+ return token, nil
50
+ }
@@ -0,0 +1,160 @@
1
+ package cloudflare
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "fmt"
8
+ "io"
9
+ "net/http"
10
+ "net/url"
11
+ "strings"
12
+ )
13
+
14
+ type Client struct {
15
+ baseURL string
16
+ token string
17
+ client *http.Client
18
+ }
19
+
20
+ type Record struct {
21
+ ID string
22
+ Type string
23
+ Name string
24
+ Content string
25
+ TTL int
26
+ Proxied bool
27
+ }
28
+
29
+ type RecordInput struct {
30
+ Type string `json:"type"`
31
+ Name string `json:"name"`
32
+ Content string `json:"content"`
33
+ TTL int `json:"ttl"`
34
+ Proxied bool `json:"proxied"`
35
+ }
36
+
37
+ func NewClient(baseURL string, token string, client *http.Client) *Client {
38
+ return &Client{
39
+ baseURL: strings.TrimRight(baseURL, "/"),
40
+ token: strings.TrimSpace(token),
41
+ client: client,
42
+ }
43
+ }
44
+
45
+ func (c *Client) ListRecords(ctx context.Context, zoneID string) ([]Record, error) {
46
+ endpoint := fmt.Sprintf("%s/zones/%s/dns_records?per_page=200", c.baseURL, url.PathEscape(zoneID))
47
+ var payload response[[]record]
48
+ if err := c.doJSON(ctx, http.MethodGet, endpoint, nil, &payload); err != nil {
49
+ return nil, err
50
+ }
51
+
52
+ out := make([]Record, 0, len(payload.Result))
53
+ for _, record := range payload.Result {
54
+ out = append(out, Record{
55
+ ID: record.ID,
56
+ Type: record.Type,
57
+ Name: record.Name,
58
+ Content: record.Content,
59
+ TTL: record.TTL,
60
+ Proxied: record.Proxied,
61
+ })
62
+ }
63
+ return out, nil
64
+ }
65
+
66
+ func (c *Client) CreateRecord(ctx context.Context, zoneID string, input RecordInput) (Record, error) {
67
+ endpoint := fmt.Sprintf("%s/zones/%s/dns_records", c.baseURL, url.PathEscape(zoneID))
68
+ var payload response[record]
69
+ if err := c.doJSON(ctx, http.MethodPost, endpoint, input, &payload); err != nil {
70
+ return Record{}, err
71
+ }
72
+ return Record{
73
+ ID: payload.Result.ID,
74
+ Type: payload.Result.Type,
75
+ Name: payload.Result.Name,
76
+ Content: payload.Result.Content,
77
+ TTL: payload.Result.TTL,
78
+ Proxied: payload.Result.Proxied,
79
+ }, nil
80
+ }
81
+
82
+ func (c *Client) UpdateRecord(ctx context.Context, zoneID string, recordID string, input RecordInput) (Record, error) {
83
+ endpoint := fmt.Sprintf("%s/zones/%s/dns_records/%s", c.baseURL, url.PathEscape(zoneID), url.PathEscape(recordID))
84
+ var payload response[record]
85
+ if err := c.doJSON(ctx, http.MethodPut, endpoint, input, &payload); err != nil {
86
+ return Record{}, err
87
+ }
88
+ return Record{
89
+ ID: payload.Result.ID,
90
+ Type: payload.Result.Type,
91
+ Name: payload.Result.Name,
92
+ Content: payload.Result.Content,
93
+ TTL: payload.Result.TTL,
94
+ Proxied: payload.Result.Proxied,
95
+ }, nil
96
+ }
97
+
98
+ func (c *Client) DeleteRecord(ctx context.Context, zoneID string, recordID string) error {
99
+ endpoint := fmt.Sprintf("%s/zones/%s/dns_records/%s", c.baseURL, url.PathEscape(zoneID), url.PathEscape(recordID))
100
+ return c.doJSON(ctx, http.MethodDelete, endpoint, nil, nil)
101
+ }
102
+
103
+ func (c *Client) doJSON(ctx context.Context, method string, endpoint string, body any, out any) error {
104
+ if c.token == "" {
105
+ return fmt.Errorf("cloudflare token is empty")
106
+ }
107
+
108
+ var reader io.Reader
109
+ if body != nil {
110
+ payload, err := json.Marshal(body)
111
+ if err != nil {
112
+ return err
113
+ }
114
+ reader = bytes.NewReader(payload)
115
+ }
116
+
117
+ req, err := http.NewRequestWithContext(ctx, method, endpoint, reader)
118
+ if err != nil {
119
+ return err
120
+ }
121
+ req.Header.Set("Authorization", "Bearer "+c.token)
122
+ req.Header.Set("Content-Type", "application/json")
123
+
124
+ resp, err := c.client.Do(req)
125
+ if err != nil {
126
+ return err
127
+ }
128
+ defer resp.Body.Close()
129
+
130
+ raw, err := io.ReadAll(resp.Body)
131
+ if err != nil {
132
+ return err
133
+ }
134
+
135
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
136
+ return fmt.Errorf("cloudflare %s %s failed: status=%d body=%s", method, endpoint, resp.StatusCode, strings.TrimSpace(string(raw)))
137
+ }
138
+
139
+ if out == nil {
140
+ return nil
141
+ }
142
+ if err := json.Unmarshal(raw, out); err != nil {
143
+ return err
144
+ }
145
+ return nil
146
+ }
147
+
148
+ type response[T any] struct {
149
+ Success bool `json:"success"`
150
+ Result T `json:"result"`
151
+ }
152
+
153
+ type record struct {
154
+ ID string `json:"id"`
155
+ Type string `json:"type"`
156
+ Name string `json:"name"`
157
+ Content string `json:"content"`
158
+ TTL int `json:"ttl"`
159
+ Proxied bool `json:"proxied"`
160
+ }
@@ -0,0 +1,23 @@
1
+ package config
2
+
3
+ import "os"
4
+
5
+ type Config struct {
6
+ Port string
7
+ DatabaseURL string
8
+ }
9
+
10
+ func Load() (Config, error) {
11
+ return Config{
12
+ Port: envOr("PORT", "8080"),
13
+ DatabaseURL: envOr("DATABASE_URL", ""),
14
+ }, nil
15
+ }
16
+
17
+ func envOr(key string, fallback string) string {
18
+ value := os.Getenv(key)
19
+ if value != "" {
20
+ return value
21
+ }
22
+ return fallback
23
+ }
@@ -0,0 +1,79 @@
1
+ package connectapi
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+
7
+ "connectrpc.com/connect"
8
+
9
+ dnsv1 "{{MODULE_PATH}}/gen/dns/v1"
10
+ dnsv1connect "{{MODULE_PATH}}/gen/dns/v1/dnsv1connect"
11
+ "{{MODULE_PATH}}/internal/app"
12
+ )
13
+
14
+ type Handler struct {
15
+ service *app.DNSService
16
+ }
17
+
18
+ func NewHandler(service *app.DNSService) (string, http.Handler) {
19
+ return dnsv1connect.NewDNSServiceHandler(&Handler{service: service})
20
+ }
21
+
22
+ func (h *Handler) ListRecords(ctx context.Context, _ *connect.Request[dnsv1.ListRecordsRequest]) (*connect.Response[dnsv1.ListRecordsResponse], error) {
23
+ records, err := h.service.ListRecords(ctx)
24
+ if err != nil {
25
+ return nil, connect.NewError(connect.CodeInternal, err)
26
+ }
27
+
28
+ response := &dnsv1.ListRecordsResponse{Records: make([]*dnsv1.Record, 0, len(records))}
29
+ for _, record := range records {
30
+ response.Records = append(response.Records, toProtoRecord(record))
31
+ }
32
+ return connect.NewResponse(response), nil
33
+ }
34
+
35
+ func (h *Handler) CreateRecord(ctx context.Context, request *connect.Request[dnsv1.CreateRecordRequest]) (*connect.Response[dnsv1.CreateRecordResponse], error) {
36
+ record, err := h.service.CreateRecord(ctx, app.CreateRecordInput{
37
+ Type: request.Msg.GetType(),
38
+ Name: request.Msg.GetName(),
39
+ Content: request.Msg.GetContent(),
40
+ TTL: int(request.Msg.GetTtl()),
41
+ Proxied: request.Msg.GetProxied(),
42
+ })
43
+ if err != nil {
44
+ return nil, connect.NewError(connect.CodeInternal, err)
45
+ }
46
+ return connect.NewResponse(&dnsv1.CreateRecordResponse{Record: toProtoRecord(record)}), nil
47
+ }
48
+
49
+ func (h *Handler) UpdateRecord(ctx context.Context, request *connect.Request[dnsv1.UpdateRecordRequest]) (*connect.Response[dnsv1.UpdateRecordResponse], error) {
50
+ record, err := h.service.UpdateRecord(ctx, request.Msg.GetId(), app.UpdateRecordInput{
51
+ Type: request.Msg.GetType(),
52
+ Name: request.Msg.GetName(),
53
+ Content: request.Msg.GetContent(),
54
+ TTL: int(request.Msg.GetTtl()),
55
+ Proxied: request.Msg.GetProxied(),
56
+ })
57
+ if err != nil {
58
+ return nil, connect.NewError(connect.CodeInternal, err)
59
+ }
60
+ return connect.NewResponse(&dnsv1.UpdateRecordResponse{Record: toProtoRecord(record)}), nil
61
+ }
62
+
63
+ func (h *Handler) DeleteRecord(ctx context.Context, request *connect.Request[dnsv1.DeleteRecordRequest]) (*connect.Response[dnsv1.DeleteRecordResponse], error) {
64
+ if err := h.service.DeleteRecord(ctx, request.Msg.GetId()); err != nil {
65
+ return nil, connect.NewError(connect.CodeInternal, err)
66
+ }
67
+ return connect.NewResponse(&dnsv1.DeleteRecordResponse{}), nil
68
+ }
69
+
70
+ func toProtoRecord(record app.Record) *dnsv1.Record {
71
+ return &dnsv1.Record{
72
+ Id: record.ID,
73
+ Type: record.Type,
74
+ Name: record.Name,
75
+ Content: record.Content,
76
+ Ttl: int32(record.TTL),
77
+ Proxied: record.Proxied,
78
+ }
79
+ }