This tutorial demonstrates how to ‘auto migrate’ the DB and how to store and retrieve data from JSONB field in postgreSQL database using GoLang.
Auto migrating the DB
In the context of Go programming language and GORM (Go Object Relational Mapping) library, automigration is a feature that automatically creates or updates database tables based on the Go struct definitions.
For the purpose of this example, we will create a table with id, username, email and meta data field. The meta data will be a JSONB field. We could use JSON as well but JSONB is stored in binary format, and although insert operations are slower searching is faster.
In general it is recommended to always use JSONB unless we have a real good reason to use JSON. For example JSON preserves formatting and allows for duplicate keys.
But before we could use JSONB with GORM we have to install the package
go get github.com/jackc/pgtype
Create a connection to PostgreSQL database.
Gorm supports different databases but here will do this exercise with PostgreSQL only.
Define the structure that will be used to create the the Users table
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"unique"`
Email string
Age int
MetaData pgtype.JSONB `gorm:"type:jsonb" json:"fieldnameofjsonb"`
}
Now let’s create two helper functions that will create a new user:
func createuserWithMetaData(db *gorm.DB, username string, email string, metaData string) User {
jsonData := pgtype.JSONB{}
err := jsonData.Set([]byte(metaData))
if err != nil {
log.Fatal(err)
}
// Create a user
newUser := User{Username: username, Email: email, Age: 36, MetaData: jsonData}
err = createUser(db, &newUser)
if err != nil {
log.Fatal(err)
}
return newUser
}
func createUser(db *gorm.DB, user *User) error {
result := db.Create(user)
if result.Error != nil {
return result.Error
}
return nil
}
And let’s put it all together:
The entire code
package main
import (
"log"
"github.com/jackc/pgtype"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"unique"`
Email string
Age int
MetaData pgtype.JSONB `gorm:"type:jsonb" json:"fieldnameofjsonb"`
}
func connectToPostgreSQL() (*gorm.DB, error) {
dsn := "user=toninichev dbname=tests host=localhost port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
return db, nil
}
func createuserWithMetaData(db *gorm.DB, username string, email string, metaData string) User {
jsonData := pgtype.JSONB{}
err := jsonData.Set([]byte(metaData))
if err != nil {
log.Fatal(err)
}
// Create a user
newUser := User{Username: username, Email: email, Age: 36, MetaData: jsonData}
err = createUser(db, &newUser)
if err != nil {
log.Fatal(err)
}
return newUser
}
func createUser(db *gorm.DB, user *User) error {
result := db.Create(user)
if result.Error != nil {
return result.Error
}
return nil
}
func getUserByID(db *gorm.DB, userID uint) (*User, error) {
var user User
result := db.First(&user, userID)
if result.Error != nil {
return nil, result.Error
}
return &user, nil
}
func updateUser(db *gorm.DB, user *User) error {
result := db.Save(user)
if result.Error != nil {
return result.Error
}
return nil
}
func deleteUser(db *gorm.DB, user *User) error {
result := db.Delete(user)
if result.Error != nil {
return result.Error
}
return nil
}
func autoMigrateDB(db *gorm.DB) {
// Perform database migration
err := db.AutoMigrate(&User{})
if err != nil {
log.Fatal(err)
}
}
func main() {
db := func() *gorm.DB {
db, err := connectToPostgreSQL()
if err != nil {
log.Fatal(err)
}
return db
}()
autoMigrateDB(db)
//CRUD operations
func() {
newUser := createuserWithMetaData(db, "Toni", "toni@gmail.com", `{"key": "value", "days":[{"dayOne": "1"}], "user-id": "1"}`)
log.Println("Created user:", newUser)
}()
func() {
newUser := createuserWithMetaData(db, "John", "john@gmail.com", `{"key": "value two", "days":[{"dayOne": "2"}], "user-id": "2"}`)
log.Println("Created user:", newUser)
}()
func() {
newUser := createuserWithMetaData(db, "Sam", "sam@gmail.com", `{"key": "value three", "days":[{"dayOne": "3"}], "user-id": "3"}`)
log.Println("Created user:", newUser)
}()
// Query user by ID
user, err := getUserByID(db, 2)
if err != nil {
log.Fatal(err)
}
log.Println("User by ID:", user)
var result User
db.Where("meta_data->>'user-id' = ?", "2").First(&result)
log.Println(result)
}
Updates
We can use byte type instead of pgtype libray which simplifies the code a bit.
type Users struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"unique"`
MetaData []byte `gorm:"type:jsonb" json:"meta-data"`
}
we are going to use Gin Web framework to create simple HTTP server that we could query against, passing JWT in the header and then using secret or public key to validate the signature.
package main
import (
"github.com/gin-gonic/gin"
)
func AuthMiddleware() gin.HandlerFunc {
// In a real-world application, you would perform proper authentication here.
// For the sake of this example, we'll just check if an API key is present.
return func(c *gin.Context) {
apiKey := c.GetHeader("X-Auth-Token")
if apiKey == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
return
}
c.Next()
}
}
func main() {
// Create a new Gin router
router := gin.Default()
// Public routes (no authentication required)
public := router.Group("/public")
{
public.GET("/info", func(c *gin.Context) {
c.String(200, "Public information")
})
public.GET("/products", func(c *gin.Context) {
c.String(200, "Public product list")
})
}
// Private routes (require authentication)
private := router.Group("/private")
private.Use(AuthMiddleware())
{
private.GET("/data", func(c *gin.Context) {
c.String(200, "Private data accessible after authentication")
})
private.POST("/create", func(c *gin.Context) {
c.String(200, "Create a new resource")
})
}
router.POST("query", AuthMiddleware(), validateSession, returnData)
// Run the server on port 8080
router.Run(":8080")
}
We added validateSession middleware that will decode the token and verify the signature.
Creating JWT services to decode and validate signature
We are using jwt GoLang library to decode the token, and validate the signature.
There are two ways to encode JWT: using symmetric encryption (meaning that the same secret is used to sign and validate the signature. This is done in retreiveTokenWithSymmetrikKey and retreiveTokenWithAsymmetrikKey is used to validate signature using the public key from the private/public key pair used to sign the token.
Make sure that you comment and uncomment the right type of token that you want to use: asymmetric vs symmetric.
Run the project yarn start end copy the long string printed right after SIGNED JWT.
Create new postman POST request
Open Postman and create new POST request. In the url put http://localhost:8080/query this is where our Gin Web server running.
Add X-Auth-Token JWT
Open header section, and add X-Auth-Token key with the value the JWT copied from Sign, Verify and decode JWT
Add query parameters and variables.
We are going to pass dummy parameters just for testing except for product
We are going to use product parameter to distinguish between symmetric and asymmetric tokens.
Let’s assume that our app will except symmetric tokens for web and asymmetric for app so make sure that you will pass the right JWT.
Navigate to the GraphQL section of the request, and add the query and the variables.
Json Web Token become widely popular for creating data with optional signature and/or optional encryption and payload.
JWTs are a Base64 encoded string with a signature attached to it. JWT components are separated by . The components are:
Header: Contains metadata about the token, such as the signing algorithm used.
Payload: Contains the claims, which are statements about the subject of the token. For example, a JWT might contain claims about a user’s identity, such as their username and email address, or their authorization to access certain resources.
Signature: A digital signature that ensures the integrity of the header and payload. The signature is created using the header and payload and a secret key known only to the issuer.
echo 'eyJhdXRob3IiOiJUb25pIFkgTmljaGV2IiwiaWF0IjoxNzA2MTEzNDc0LCJkYXRhIjoiTmV3IEpXVCBnZW5lcmF0ZWQgYXQgV2VkIEphbiAyNCAyMDI0IDExOjI0OjM0IEdNVC0wNTAwIChFYXN0ZXJuIFN0YW5kYXJkIFRpbWUpIiwiZXhwIjoxNzA2MTU2Njc0LCJhdWQiOiJodHRwczovL215c29mdHdhcmUtY29ycC5jb20iLCJpc3MiOiJUb25pIE5pY2hldiIsInN1YiI6InRvbmkubmljaGV2QGdtYWlsLmNvbSJ9' |base64 -d
{"author":"Toni Y Nichev","iat":1706113474,"data":"New JWT generated at Wed Jan 24 2024 11:24:34 GMT-0500 (Eastern Standard Time)","exp":1706156674,"aud":"https://mysoftware-corp.com","iss":"Toni Nichev","sub":"toni.nichev@gmail.com"}
As we see JWT payload is not encrypted and could be decoded with any base64 decoder so never store sensitive data there. The purpose of signing with our private key is to make sure that ‘audience’ (who ever is going to use the token) will be able to verify the authenticity of this token with shared public key.
Claims to Verify
When code is presented with a JWT, it should verify certain claims. At a minimum, these claims should be checked out:
iss identifies the issuer of the JWT. (UUID, domain name, URL or something else)
aud identifies the audience of the token, that is, who should be consuming it. aud may be a scalar or an array value.
nbf and exp. These claims determine the timeframe for which the token is valid.
It doesn’t matter exactly what this strings are as long as the issuer and consumer of the JWT agree on the values.
JWT signing algorithms.
The default algorithm used is (HS256) which is symmetric: meaning that the same ‘secret’ is used for signing and verifying. In the example below `itsasecret123`
Once you create application, you have to create some products to sell.
In general there are these types of products supported by Apple’s App Store:
Consumables.
Consumables are products that can be used (‘consumed’) once and then repurchased multiple times.
Non-consumables
Non-consumables are products that are purchased once, have no expiry date, and remain permanently available within your app.
Auto-renewal subscriptions
Auto-renewal subscriptions are products or services that you pay for on a recurring basis. Developers love these, as they guarantee a steady income stream.
Non auto-renewal subscriptions.
Non auto-renewal subscriptions are those that run for a fixed period of time, after which you can choose to manually renew. Often, these last longer and cost more than auto-renewal subscriptions.
To create products navigate to the newly created app, and locate “MONETIZATION” on the left side. From there you could select to create “in-app-purchases” or “Subscriptions”
Refer to Apple documentation of how to create products: In-App purchase
Creating Sandbox testing users
Ahh, yeah … you need test users so you won’t be charged with each test.
Create new Xcode project and change the bundle id to the one that you set up above in the app store connect. (Refer to Creating application bundle in App Store connect in this article.)
In-app purchase is not enabled by default. Let’s add it in Signing & Capabilities
Add mainStore.storekit file by going to files->new and look for StoreKit2.
– Name it mainStore.storekit
– Make sure that you select ‘synced‘ so it will pull data from the actual App Store and select your apple bundle id (this is how the StoreKit2 will know which items to pull)4. Let’s add another Swift class which will be our shared Store.
store.swift
import StoreKit
@MainActor final class Store: ObservableObject {
// use the same ids that you defined in the App Store connect
private var productIDs = ["InAppPurchaseTutorialToniconsumableFuel", "InAppPurchaseTutorialToniConsumableOil", "BrakePads", "InAppPurchaseRenewalPro"]
private var updates: Task<Void, Never>?
@Published var products = [Product]()
@Published var activeTransactions: Set<StoreKit.Transaction> = []
init() {
Task {
await requestProducts()
}
Task {
await transactionUpdates()
}
}
deinit {
updates?.cancel()
}
func transactionUpdates () async {
for await update in StoreKit.Transaction.updates {
if let transaction = try? update.payloadValue {
activeTransactions.insert(transaction)
await transaction.finish()
}
}
}
func requestProducts() async {
do {
products = try await Product.products(for: productIDs)
} catch {
print(error)
}
}
func purchaseProduct(_ product: Product) async throws {
print("Purchase tapped ...")
let result = try await product.purchase()
switch result {
case .success(let verifyResult):
print("Purchase successfull!")
if let transaction = try? verifyResult.payloadValue {
activeTransactions.insert(transaction)
print("PURCHASE:")
try print(verifyResult.payloadValue)
print(transaction)
await transaction.finish()
}
case .userCancelled:
print("Purchase Canceled !")
break
case .pending:
print("Purchase pending ...")
break
@unknown default:
break
}
}
func fetchActiveTransactions() async {
var activeTransactions: Set<StoreKit.Transaction> = []
for await entitelment in StoreKit.Transaction.currentEntitlements {
if let transaction = try? entitelment.payloadValue {
activeTransactions.insert(transaction)
print("fetchActiveTransactions: ")
print(transaction)
}
}
}
}
In productIDs use the same ids that you defined in the App Store connect.
5. Edit the View to add purchase buttons.
Views/ContentView.swift
//
// ContentView.swift
// InAppPurchaseTutorial
//
// Created by Toni Nichev on 1/2/24.
//
import SwiftUI
import StoreKit
struct ContentView: View {
@EnvironmentObject var store: Store
var body: some View {
VStack {
Text("Welcome to my store").font(.title)
ProductView(id: "InAppPurchaseTutorialToniconsumableFuel") {
Image(systemName: "crown")
}
.productViewStyle(.compact)
.padding()
.onInAppPurchaseCompletion { product, result in
if case .success(.success(let transaction)) = result {
print("Purchased successfully: \(transaction.signedDate)")
} else {
print("Something else happened")
}
}
}
Section(header: Text("To buy:").font(.title)) {
ForEach(store.products, id: \.id) { product in
Button {
Task {
try await store.purchaseProduct(product)
}
} label: {
HStack {
Text(product.displayName + ":")
Text(verbatim: product.displayPrice)
}
}
.buttonStyle(.borderedProminent)
}
}
}
}
#Preview {
ContentView().environmentObject(Store())
}
6. Last part is to add method to retreive purchases when the application starts.
InAppPurchaseTutorialApp.swift
//
// InAppPurchaseTutorialApp.swift
// InAppPurchaseTutorial
//
// Created by Toni Nichev on 1/2/24.
//
import SwiftUI
@main
struct InAppPurchaseTutorialApp: App {
@Environment(\.scenePhase) private var sceneParse
@StateObject private var store = Store()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(store)
.task(id: sceneParse) {
if sceneParse == .active {
await store.fetchActiveTransactions()
}
}
}
}
}
Setting up Backend server to listen to server-2-server notifications from Apple server
Apple server sends server-to-server notifications in real time when subscription event happens, like subscribed, cancel subscription, did renew etc.
Mode details of all notification types could be found in Apple documentation
Purpose: we can receive these notifications on transaction complete, on purchase made, on subscriptions: purchased, canceled, failed and we could grant access to subscribers to some paid services like (pro news, premium game levels etc.)
Create your backend server handler, that Apple App Store server will notify.
I like to use PHP version cause it’s super easy to implement but any language works the same way:index.php
what we just did:
– decoded JWT
– Saved the decoded response in the data.txt file
so far so good, but we need to register our server-listener to the App-store connect so the Apple server will notify our server.
Add your backend server url to Apps’ App Store Server notifications
Now we could navigate to the physical phone, run the app and make a purchase.
Make sure that once prompted for the password you add the test account password.
Now we have server-2-server notification set up, but if we want to use more of Apple’s APIs (like transaction history, purchases, etc) we have to create an app to generate signed JWT.
Apple won’t accept JWT that lives longer than 20 min. so we have to make sure that this is the maximup ttl that we set up.
const expirationTime = now + 900; // Set to 15 minutes (900 seconds)
There are two types of Apple APIs like it was stated above:
If you don’t already have CocoaPods installed, follow the steps in the CocoaPods Getting Started guide.
Open a terminal window and navigate to the location of your app’s Xcode project.
If you have not already created a Podfile for your application, create one now:
pod init
Open the Podfile created for your application and add the following:
pod 'GoogleSignIn'
If you are using SwiftUI, also add the pod extension for the “Sign in with Google” button:
pod 'GoogleSignInSwiftSupport'
Save the file and run:
pod install
From now on Open the generated .xcworkspaceworkspace file for your application in Xcode. Use this file for all future development on your application. (Note that this is different from the included .xcodeprojproject file, which would result in build errors when opened.)
Now we are almost ready to start coding, but when we build the project we might (or might not depends of X-code version) face some issues..
Navigate to the Build Settings, find ‘User Script Sandboxing’ and
Flip it to No
Fixing “Your app is missing support for the following URL schemes:”
Copy missing scheme from the error message and add it in the info->url section
Let’s get started
Adding Google Client ID (GIDClientID)
Ether you face the problems before or not this is one thing that is mandatory.
1. Adding UserAuthModel to share between all views.
If you don’t know how to do this read about ObservableObject and @Published and sharing data between Views.
This class has to conform to the ObservableObject in order to have its properties reflecting the View.
We will create methods to check if user is signed in, and update shared parameters: givenName, userEmail, isLoggedIn …
import SwiftUI
import GoogleSignIn
import GoogleSignInSwift
final class UserAuthModel: ObservableObject {
@Published var givenName: String = ""
@Published var isLoggedIn: Bool = false
@Published var errorMessage: String = ""
@Published var userEmail: String = ""
@Published var profilePicUrl: String = ""
init() {
check()
}
func getUserStatus() {
if GIDSignIn.sharedInstance.currentUser != nil {
let user = GIDSignIn.sharedInstance.currentUser
guard let user = user else { return }
let givenName = user.profile?.givenName
self.givenName = givenName ?? ""
self.userEmail = user.profile!.email
self.profilePicUrl = user.profile!.imageURL(withDimension: 100)!.absoluteString
self.isLoggedIn = true
} else {
self.isLoggedIn = false
self.givenName = "Not Logged In"
}
}
func check() {
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
if let error = error {
self.errorMessage = "error: \(error.localizedDescription)"
}
self.getUserStatus()
}
}
func gertRootViewController() -> UIViewController {
guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
return .init()
}
guard let root = screen.windows.first?.rootViewController else {
return .init()
}
return root
}
func signIn() {
GIDSignIn.sharedInstance.signIn(withPresenting: gertRootViewController()) { signInResult, error in
guard let result = signInResult else {
// Inspect error
print("Error occured in signIn()")
return
}
print("Signing in ...")
print(result.user.profile?.givenName ?? "")
self.getUserStatus()
}
}
func signOut() {
GIDSignIn.sharedInstance.signOut()
self.getUserStatus()
}
Now let’s edit the app starter and put userAuthModel in the environmentObject
//
// SignInWithGoogleTutorialApp.swift
// SignInWithGoogleTutorial
//
// Created by Toni Nichev on 1/3/24.
//
import SwiftUI
@main
struct SignInWithGoogleTutorialApp: App {
@StateObject var userAuthModel: UserAuthModel = UserAuthModel()
var body: some Scene {
WindowGroup {
NavigationView {
ContentView()
}
.environmentObject(userAuthModel)
}
}
}
The purpose of authentication on the backend server is to make sure that logged-in users could have access to some protected content, like subscriptions, pro-articles, etc.
Once the user signs-in in the native app, the app sends the id-token to the backend, and the backend validates the token and could return access-token back to the app.
In the previous chapter we added UserAuthModel.swift file.
This is the place to call the backend server.
func sendTokenToBackendServer() {
let user = GIDSignIn.sharedInstance.currentUser
guard let user = user else { return }
let stringToken = user.idToken!.tokenString
guard let authData = try? JSONEncoder().encode(["idToken" : stringToken]) else {
return
}
let url = URL(string: "https://regexor.net/examples/google-sign-in-server-notification/")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.uploadTask(with: request, from: authData) { data, response, error in
print(response ?? ".")
// handle response from my backend.
if error != nil {
print("Error: \(String(describing: error))")
}
// Handle the response from the server
let dataString = String(data: data!, encoding: .utf8)
print ("got data: \(dataString!)")
}
task.resume()
}
and the final UserAuthenticationModel.swift will look like this:
import SwiftUI
import GoogleSignIn
import GoogleSignInSwift
final class UserAuthModel: ObservableObject {
@Published var givenName: String = ""
@Published var isLoggedIn: Bool = false
@Published var errorMessage: String = ""
@Published var userEmail: String = ""
@Published var profilePicUrl: String = ""
init() {
check()
}
func getUserStatus() {
if GIDSignIn.sharedInstance.currentUser != nil {
let user = GIDSignIn.sharedInstance.currentUser
guard let user = user else { return }
let givenName = user.profile?.givenName
self.givenName = givenName ?? ""
self.userEmail = user.profile!.email
self.profilePicUrl = user.profile!.imageURL(withDimension: 100)!.absoluteString
self.isLoggedIn = true
} else {
self.isLoggedIn = false
self.givenName = "Not Logged In"
}
}
func check() {
GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
if let error = error {
self.errorMessage = "error: \(error.localizedDescription)"
}
self.getUserStatus()
}
}
func gertRootViewController() -> UIViewController {
guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
return .init()
}
guard let root = screen.windows.first?.rootViewController else {
return .init()
}
return root
}
func signIn() {
GIDSignIn.sharedInstance.signIn(withPresenting: gertRootViewController()) { signInResult, error in
guard let result = signInResult else {
// Inspect error
print("Error occured in signIn()")
return
}
print("Signing in ...")
print(result.user.profile?.givenName ?? "")
self.getUserStatus()
self.sendTokenToBackendServer()
}
}
func signOut() {
GIDSignIn.sharedInstance.signOut()
self.getUserStatus()
}
func sendTokenToBackendServer() {
let user = GIDSignIn.sharedInstance.currentUser
guard let user = user else { return }
let stringToken = user.idToken!.tokenString
guard let authData = try? JSONEncoder().encode(["idToken" : stringToken]) else {
return
}
let url = URL(string: "https://regexor.net/examples/google-sign-in-server-notification/")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let task = URLSession.shared.uploadTask(with: request, from: authData) { data, response, error in
print(response ?? ".")
// handle response from my backend.
if error != nil {
print("Error: \(String(describing: error))")
}
// Handle the response from the server
let dataString = String(data: data!, encoding: .utf8)
print ("got data: \(dataString!)")
}
task.resume()
}
}
Server script to get idToken form the native app:
In the example below we Just save the token to a file. In real life scenario, here we have to verify the identity of the id token before sending the access-token back to the app.
<?php
// SAVE RAW DATA
$appleData = file_get_contents('php://input');
// Just saves the token to a file.
// In real life scenario, here we have to verify the identity of the id token before sending the access-token back to the app
$file = fopen("./data.txt", "a");
fwrite($file, $appleData);
fclose($file);
echo "send something back to the native app like acccess-token";
//
// ContentView.swift
// Test
//
// Created by Toni Nichev on 1/3/24.
//
import SwiftUI
// Our observable object class
class GameSettings: ObservableObject {
@Published var scoree = 0
var test = 4
}
// A view that expects to find a GameSettings object
// in the environment, and shows its score.
struct ScoreView: View {
// 2: We are not instantiating gameSetting here since it's already done in ContentView.
@EnvironmentObject var gameSettings: GameSettings
var body: some View {
Text("Score: \(gameSettings.scoree)")
Text("Test: \(gameSettings.test)")
}
}
struct ContentView: View {
// 1: We instantiate GameSettings only here and pass it to the environmentObject at the end
@StateObject var gameSettings = GameSettings()
var body: some View {
NavigationStack {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Button("Increase score") {
gameSettings.scoree += 1
gameSettings.test += 1
}
NavigationLink {
ScoreView()
} label: {
Text("Show score")
}
}
}
.environmentObject(gameSettings)
}
}
#Preview {
ContentView()
}
#Preview {
ScoreView().environmentObject(GameSettings())
}
The goal: setting up GraphQL server using Gqlgen library.
We are going to set up GraphQL server with Users, and create queries to retrieve the users by id or user name.
Schema-first approach – means that instead of using library apis (code-first approach) we are going to write our schema manually using the GraphQL schema definition language.
Setting up the project
Create project directory mkdir gqlgen-tutorial
Navigate to the folder cd gqlgen-tutorial
Initialize go project. go mod init gqlgen-tutorial
Initializing Gqlgen with boilerplate schema and resolvers
gqlgen has handy command to Initialize gqlgen config and generate the models.
go run github.com/99designs/gqlgen init
This will create server.go the server starting point and ./graph directory with a couple of files including schema.graphqls and if you open it you will see that it comes with pre-defined example schema which we are going to remove later and start from scratch.
graph/model/model_gen.go – is auto generated file containing structure of defined by the schema file graph/schema.graphqls
graph/generated.go – this is a file with generated code that injects context and middleware for each query and mutation.
We should not modify these files since they will be modified by gqlgen as we update the schema. Instead, we should edit these files:
graph/schema.graphqls – GraphQL schema file where types, queries, and mutations are defined.
graph/resolver.go – resolver functions for queries and mutations defined in schema.graphqls
At this point we could start the server and see the playground and the schema.
go run ./server.go
Sometimes you might see an error and in this case just run go mod tidy again.
Defining queries
First let’s remove boilerplate schema from graph/schema.graphqls and add our new schema
graph/schema.graphqls
# GraphQL schema example
#
# https://gqlgen.com/getting-started/
type User {
id: ID!
name: String!
userType: String!
}
type Query {
getUser(id:ID!): User
}
input NewUser {
userId: String!
userName: String!
userType: String!
}
type Mutation {
createUser(input: NewUser!): User!
}
Generate code and running the API
We will now generate code, which will update the following files using the information we provided in the schema file:
schema.resolvers.go
model/models_gen.go
generated/generated.go
Delete the example code in schema.resolvers.go and then run the following command:
go run github.com/99designs/gqlgen generate
If we run the server we will run into an error because we didn’t define any resolver yet.
Defining the backend to fetch and store values
In resolver.go:
– import qlgen-tutorial/graph/model. line: 3
– declare a Hash Map that we will use to store users. Line 10
package graph
import "gqlgen-tutorial/graph/model"
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
type Resolver struct{
UsersStore map[string]model.User
}
We defined UserStore of type map which essentially is a hash-map with keys of type string and values of type model.User.
In schema.resolvers.go, we are going to modify the boilerplate methods: CreateUser and GetUser
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.24
import (
"context"
"fmt"
"gqlgen-tutorial/graph/model"
)
// CreateUser is the resolver for the createUser field.
func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
// create new user to be added in r.Resolver.UsersStore and returned
var user model.User
if len(r.Resolver.UsersStore) == 0 {
// create new UserStore hash map if it does not exist
r.Resolver.UsersStore = make(map[string]model.User)
}
// set up new user attributes
user.ID = input.UserID
user.Name = input.UserName
user.UserType = input.UserType
// adds newly created user into the resolver's UserStore
r.Resolver.UsersStore[input.UserID] = user
return &user, nil
}
// GetUser is the resolver for the getUser field.
func (r *queryResolver) GetUser(ctx context.Context, id string) (*model.User, error) {
fmt.Println(r.Resolver.UsersStore)
// retrieve user from the store
user, isOk := r.Resolver.UsersStore[id]
if !isOk {
return nil, fmt.Errorf("not found")
}
return &user, nil
}
// Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
GetUser is pretty straight forward: it gets user id and returns the user from the hash map store.
CreateUser first checks if UserStore is initialized. Line 19. If CreateUser length is 0 it initializes the hash-map. Line 21.
Then it gets the values for username, id and userType from the input parameter and sets up the new user, stores it into the UserStore, line 29, and returns it.
Testing
Creating user
Mutation:
mutation createUserMutation($input: NewUser!) {
createUser(input: $input) {
name
id
}
}
This way after clicking on log-in button user is redirected to the sign-in page with all sign-in providers to choose from. Clicking on “continue with …” redirects to provider’s web ui and then redirects back to out web app.
The string "PAYPALISHIRING" is written in a zigzag pattern on a given number of rows like this: (you may want to display this pattern in a fixed font for better legibility)
P A H N
A P L S I I G
Y I R
And then read line by line: "PAHNAPLSIIGYIR"
Write the code that will take a string and make this conversion given a number of rows:
It’s take the first example string: “PAYPALISHIRING”
An array representation of the string would be done by pushing each character into the array.
Then if we want to iterate through this array in zig zag pattern with 3 rows, it will look like this:
Then, we have to create 3 arrays representing the 3 rows as follows:
Dots represent empty strings which we have to ignore before concatenating all 3 rows, convert them to string and this is the answer to this problem.
But how to create the loop to traverse through the zig zag pattern (figure 1) ?
first we set up `headingDown` flag to determine if we are going down or up
each time when the row reaches numRows we are flipping the value of headingDown
when headingDown is false (meaning that we are heading up) we have to do it diagonally: row — , col ++
when row become 0 we are flipping again headingDown and it becomes true again: row ++ and we keep storing values of every character we traversed in the result array.
/**
* @param {string} s
* @param {number} numRows
* @return {string}
*/
var convert = function(s, numRows) {
// if we don't have any rows or there is an empty string just return back empty string
if(s.length === 0 || numRows === 0)
return '';
// if rows are numRows is just one, we return the same string
if(numRows === 1) {
return s;
}
var l = s.length;
// put the string into single dimension array to iterate through
var arr = s.split('');
var rowsArray = [];
var row = 0;
var col = 0;
var headingDown = true; // this determines if the cursor is heading down in the same column, or up leaping to the next column (dizgonal)
// instantiate numRows arrays to store values of each row
for(var i = 0; i < numRows; i ++) {
rowsArray[i] = [];
}
// loop through each element in arr and fill rowsArray ()
for(var i = 0; i < l; i ++) {
rowsArray[row][col] = arr[i];
if(headingDown) {
row ++;
}
else {
row --;
col ++;
}
if(row == numRows -1) {
headingDown = false;
} else if(row == 0) {
headingDown = true;
}
}
// Read 2D array and assemble the string
var result = [];
for(var i = 0; i < numRows; i ++) {
for(var j = 0; j < rowsArray[i].length; j ++) {
if(typeof rowsArray[i][j] != 'undefined') {
result.push(rowsArray[i][j]);
}
}
}
return result.join('');
};
convert('PAYPALISHIRING', 3);
convert('AB', 1);
//convert('ABC', 1);
How can we optimize this ?
the above example is good to read but not a good practical example. First we don’t need two dimensional array to store characters for each row. We could replace this with string array and just keep adding characters till we reached the end of the string. This way we also don’t need to keep track of cols
/**
* @param {string} s
* @param {number} numRows
* @return {string}
*/
var convert = function(s, numRows) {
// if we don't have any rows or there is an empty string just return back empty string
if(s.length === 0 || numRows === 0)
return '';
// if rows are numRows is just one, we return the same string
if(numRows === 1) {
return s;
}
var l = s.length;
// put the string into single dimension array to iterate through
var arr = s.split('');
var left = 0;
var arrStrings = [];
var row = 0;
var col = 0;
var headingDown = true; // this determines if the cursor is heading down in the same column, or up leaping to the next column (dizgonal)
// instantiate numRows arrays to store values of each row
for(var i = 0; i < numRows; i ++) {
arrStrings[i] = '';
}
// loop through each element in arr and fill arrStrings ()
for(var i = 0; i < l; i ++) {
//arrStrings[row][col] = arr[i];
if(headingDown) {
arrStrings[row] += arr[i];
row ++;
}
else {
arrStrings[row] += arr[i];
row --;
col ++;
}
if(row == numRows -1) {
headingDown = false;
} else if(row == 0) {
headingDown = true;
}
}
var result = '';
// combine all strings and return as one
for(var i = 0; i < numRows; i ++) {
result += arrStrings[i];
}
return result;
};