diff --git a/README.md b/README.md index 50e373e..4af55d1 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,7 @@ This project demonstrates using saga (effectively a distributed transaction) that interacts with multiple services and has a robust, edge-case-proof rollback strategy, as well as durable function -execution. - -**Temporal abstracts away failures.** - -The upstream microservices that are called during the workflow all use gRPC. +execution. **Temporal abstracts away failures.** ## Resources: @@ -40,8 +36,9 @@ make > [!NOTE] > -> Under the hood, we use [pkgx][pkgx] to run Temporal's [dev -> server][temporal-cli], and Docker to run [Jaeger][jaeger] (a telemetry +> Under the hood, we use [pkgx][pkgx] to run a bunch of internal tools, +> including Temporal's [dev server][temporal-cli]. We use Docker to run the +> infrastructure components, such as Postgres and [Jaeger][jaeger] (a telemetry > backend). [temporal-cli]: https://github.com/temporalio/cli @@ -134,23 +131,57 @@ Every endpoint is also accessible via a GraphQL API powered by ```shell pkgx tailcall start \ ./tailcall/server.graphql \ - ./tailcall/org.graphql + ./tailcall/org.graphql \ + ./tailcall/profile.graphql \ + ./tailcall/license.graphql ``` A GraphQL Playground will launch automatically for you. -Let's create an Org: +Let's create some data: ```graphql -mutation CreateOrg { +mutation Create { createOrg( input: { - id: "e0d77fa9-faa5-4a7d-83a3-92fe3a83544c" + id: "00000000-0000-0000-0000-000000000000" name: "Kevin's Org" } ) { - id - name + org { + id + name + } + } + + createProfile( + input: { + id: "00000000-0000-0000-0000-000000000000" + fullName: "Kevin Chen" + orgId: "00000000-0000-0000-0000-000000000000" + } + ) { + profile { + id + fullName + orgId + } + } + + createLicense( + input: { + id: "00000000-0000-0000-0000-000000000000" + start: "2024-01-01T15:50-04:00" + end: "2024-01-07T15:50-04:00" + userId: "00000000-0000-0000-0000-000000000000" + } + ) { + license { + id + start + end + userId + } } } ``` @@ -160,8 +191,10 @@ and then retrieve it: ```graphql query GetOrg { org(id: "e0d77fa9-faa5-4a7d-83a3-92fe3a83544c") { - id - name + org { + id + name + } } } ``` diff --git a/cmd/svc/license/service/service.go b/cmd/svc/license/service/service.go index 48560ca..8f2df35 100644 --- a/cmd/svc/license/service/service.go +++ b/cmd/svc/license/service/service.go @@ -70,9 +70,21 @@ func (s *Service) CreateLicense( err = license.Upsert(ctx, s.db, true, []string{models.LicenseColumns.ID}, boil.Infer(), boil.Infer()) if err != nil { + log.Error("Failed to create License", + "id", req.Msg.GetId(), + "err", err, + ) + return nil, fmt.Errorf("unable to insert record: %w", err) } + log.Info("Successfully created License.", + "id", req.Msg.GetId(), + "user_id", req.Msg.GetUserId(), + "start", req.Msg.GetStart(), + "end", req.Msg.GetEnd(), + ) + res := &licensev1beta1.CreateLicenseResponse{ License: &licensev1beta1.License{ Id: license.ID, diff --git a/cmd/svc/profile/service/service.go b/cmd/svc/profile/service/service.go index 5cbdda3..c9801a5 100644 --- a/cmd/svc/profile/service/service.go +++ b/cmd/svc/profile/service/service.go @@ -40,7 +40,7 @@ func (s *Service) CreateProfile( log.Info("Creating Profile...", "id", req.Msg.GetId(), "org_id", req.Msg.GetOrgId(), - "name", req.Msg.GetFullName(), + "full_name", req.Msg.GetFullName(), ) profile := models.Profile{ @@ -51,9 +51,20 @@ func (s *Service) CreateProfile( err := profile.Upsert(ctx, s.db, true, []string{models.ProfileColumns.ID}, boil.Infer(), boil.Infer()) if err != nil { + log.Error("Failed to create Profile", + "id", req.Msg.GetId(), + "err", err, + ) + return nil, fmt.Errorf("unable to insert record: %w", err) } + log.Info("Successfully created Profile.", + "id", req.Msg.GetId(), + "org_id", req.Msg.GetOrgId(), + "full_name", req.Msg.GetFullName(), + ) + res := &profilePB.CreateProfileResponse{ Profile: &profilePB.Profile{ Id: profile.ID, diff --git a/tailcall/license.graphql b/tailcall/license.graphql new file mode 100644 index 0000000..6fe1838 --- /dev/null +++ b/tailcall/license.graphql @@ -0,0 +1,35 @@ +type Timestamp { + seconds: Int! + nanos: Int! +} + +input TimestampInput { + seconds: Int! + nanos: Int! +} + +type License { + id: ID! + start: Timestamp! + end: Timestamp! + userId: String! +} + +input GetLicenseRequest { + id: String! +} + +type GetLicenseResponse { + license: License! +} + +input CreateLicenseInput { + id: ID! + start: TimestampInput! + end: TimestampInput! + userId: String! +} + +type CreateLicenseResponse { + license: License! +} diff --git a/tailcall/org.graphql b/tailcall/org.graphql index eccf18b..a78066f 100644 --- a/tailcall/org.graphql +++ b/tailcall/org.graphql @@ -1,4 +1,3 @@ - type Org { id: ID! name: String! diff --git a/tailcall/profile.graphql b/tailcall/profile.graphql new file mode 100644 index 0000000..203e8a4 --- /dev/null +++ b/tailcall/profile.graphql @@ -0,0 +1,23 @@ +type Profile { + id: ID! + fullName: String! + orgId: String! +} + +input GetProfileRequest { + id: String! +} + +type GetProfileResponse { + profile: Profile! +} + +input CreateProfileInput { + id: ID! + fullName: String! + orgId: String! +} + +type CreateProfileResponse { + profile: Profile! +} diff --git a/tailcall/server.graphql b/tailcall/server.graphql index 6eeb77c..d24ddea 100644 --- a/tailcall/server.graphql +++ b/tailcall/server.graphql @@ -21,6 +21,18 @@ type Query { baseURL: "http://localhost:9090" path: "/orgs/{{args.id}}" ) + + profile(id: ID!): GetProfileResponse! + @http( + baseURL: "http://localhost:9091" + path: "/profiles/{{args.id}}" + ) + + license(id: ID!): GetLicenseResponse! + @http( + baseURL: "http://localhost:9092" + path: "/licenses/{{args.id}}" + ) } type Mutation { @@ -31,4 +43,20 @@ type Mutation { path: "/orgs/{{args.input.id}}" body: "{{args.input}}" ) + + createProfile(input: CreateProfileInput!): CreateProfileResponse! + @http( + baseURL: "http://localhost:9091" + method: "POST" + path: "/profiles/{{args.input.id}}" + body: "{{args.input}}" + ) + + createLicense(input: CreateLicenseInput!): CreateLicenseResponse! + @http( + baseURL: "http://localhost:9092" + method: "POST" + path: "/licenses/{{args.input.id}}" + body: "{{args.input}}" + ) }