برنامه نویسیبک اند

ساخت یک GraphQL API در Go برای آپلود فایل‌ها به Spaces

مقدمه

برای بسیاری از اپلیکیشن‌ها، یک قابلیت مطلوب، توانایی کاربر در آپلود تصویر پروفایل است. با این حال، ساخت این قابلیت برای توسعه‌دهندگان تازه‌وارد GraphQL — که پشتیبانی داخلی از آپلود فایل ندارد — می‌تواند چالش‌برانگیز باشد.

در این آموزش، یاد می‌گیرید تصاویر را مستقیماً از اپلیکیشن Backend خود به یک سرویس ذخیره‌سازی شخص ثالث آپلود کنید. یک GraphQL API می‌سازید که از SDK سازگار با S3 مربوط به AWS برای Go استفاده می‌کند تا از یک Backend نوشته‌شده با Go، تصاویر را به Spaces پارمین کلود — یک سرویس ذخیره‌سازی Object مقیاس‌پذیر — آپلود کند. Backend مربوط به Go یک GraphQL API ارائه کرده و داده‌های کاربر را در یک پایگاه داده PostgreSQL ذخیره می‌کند که توسط سرویس پایگاه داده مدیریت‌شده پارمین کلود فراهم شده است. برای یک رویکرد جایگزین با Node.js، مقاله «ساخت و استقرار یک سرور GraphQL» را ببینید.

تا پایان این آموزش، یک GraphQL API با Golang ساخته‌اید که می‌تواند یک فایل رسانه‌ای را از یک درخواست Multipart HTTP دریافت کرده و فایل را به یک Bucket داخل Spaces پارمین کلود آپلود کند.

محیط تست‌شده

این آموزش با نسخه‌های زیر تست شده است:

  • Go نسخه ۱.۲۲ به بالا
  • gqlgen نسخه v0.17.49
  • aws-sdk-go-v2 نسخه v1.26 به بالا
  • go-pg نسخه v10

قبل از ادامه، نسخه نصب‌شده Go خود را با go version تأیید کنید.

نکات کلیدی

  • GraphQL به‌صورت بومی از آپلود فایل پشتیبانی نمی‌کند. graphql-multipart-request-spec استاندارد جامعه است و gqlgen آن را از طریق Scalar با نام Upload پیاده‌سازی می‌کند.
  • AWS SDK Go v2 نسخه SDK پشتیبانی‌شده فعلی برای تعامل با ذخیره‌سازی سازگار با S3 — شامل Spaces پارمین کلود — است.
  • فایل‌های ارسالی از طریق Mutationهای Multipart گراف‌کیوال، در Resolver به‌صورت io.Reader می‌رسند. آن‌ها را با io.ReadAll بخوانید یا مستقیماً به ذخیره‌سازی استریم کنید.
  • آپلود سمت سرور (این آموزش) پیاده‌سازی ساده‌تری دارد. URLهای Presigned بار سرور را برای فایل‌های بزرگ کاهش می‌دهند.
  • فایل‌ها با io.ReadAll مستقیماً در حافظه خوانده شده و از طریق bytes.NewReader به PutObject پاس می‌شوند. برای فایل‌های خیلی بزرگ، input.File.File را مستقیماً به فیلد Body استریم کنید تا از مصرف بالای حافظه اجتناب شود.

پیش‌نیازها

برای دنبال کردن این آموزش به موارد زیر نیاز دارید:

  • یک حساب کاربری پارمین کلود. اگر ندارید، ثبت‌نام کنید. در این آموزش از Spaces و پایگاه داده مدیریت‌شده پارمین کلود استفاده می‌کنید.
  • یک Space در پارمین کلود همراه با Access Key و Access Secret که می‌توانید با دنبال‌کردن راهنمای «Space و API Key» بسازید. همچنین می‌توانید مستندات «مدیریت دسترسی» را ببینید.
  • Go نصب‌شده روی ماشین محلی خود، که می‌توانید با دنبال‌کردن مجموعه راهنماهای راه‌اندازی Go این کار را انجام دهید.
  • دانش پایه Golang که می‌توانید از مجموعه آموزش‌های Go ما به دست آورید. آموزش «اولین برنامه Go» معرفی خوبی فراهم می‌کند.
  • درک GraphQL که می‌توانید آن را در آموزش «مقدمه‌ای بر GraphQL» پیدا کنید.

درک GraphQL Multipart Request Specification

GraphQL مکانیزم بومی برای آپلود فایل تعریف نمی‌کند. graphql-multipart-request-spec (نوشته jaydenseric) استاندارد de facto جامعه برای ارسال فایل‌ها از طریق GraphQL با درخواست‌های Multipart HTTP است.

این Specification یک Payload فرم سه‌بخشی تعریف می‌کند:

  • operations: کوئری یا Mutation گراف‌کیوال به‌صورت رشته JSON، با متغیرهای فایل روی null تنظیم‌شده.
  • map: یک آبجکت JSON که هر بخش فایل را به مسیر متغیرش در operations نگاشت می‌کند.
  • بخش‌های واقعی فایل که با کلیدهای map به آن‌ها ارجاع داده می‌شوند.

gqlgen این Specification را به‌صورت بومی از طریق نوع Scalar با نام Upload پیاده‌سازی می‌کند. جریان درخواست به این شکل است:

Client (multipart/form-data)
    │
    ├── operations: { query, variables: { image: null } }
    ├── map: { "0": ["variables.image"] }
    ├── 0: <binary file data>
    │
    ▼
Go HTTP Server (net/http)
    │
    ▼
gqlgen Handler (parses multipart, maps Upload scalar)
    │
    ▼
UploadProfileImage Resolver
    │
    ▼
AWS SDK Go v2 ──► Parmin Cloud Spaces (S3-compatible)

با این دید، می‌توانید GraphQL API را راه‌اندازی اولیه (Bootstrap) کرده و Schema را طوری تعریف کنید که متغیرهای فایل از Scalar با نام Upload استفاده کنند.

مرحله ۱ — راه‌اندازی اولیه یک GraphQL API در Golang

در این مرحله، از کتابخانه Gqlgen برای راه‌اندازی اولیه GraphQL API استفاده می‌کنید. Gqlgen یک کتابخانه Go برای ساخت GraphQL APIها است. دو قابلیت مهمی که Gqlgen فراهم می‌کند، رویکرد Schema-First و تولید کد (Code Generation) است. با رویکرد Schema-First، ابتدا مدل داده API را با زبان تعریف Schema گراف‌کیوال (SDL) تعریف می‌کنید. سپس کد Boilerplate برای API را از Schema تعریف‌شده تولید می‌کنید. با استفاده از قابلیت تولید کد، نیازی به ساخت دستی Resolverهای Query و Mutation برای API ندارید؛ چون آن‌ها به‌طور خودکار تولید می‌شوند.

برای شروع، دستور زیر را برای نصب gqlgen اجرا کنید. این آموزش یک نسخه پایدار را برای تکرارپذیری Pin کرده است. برای آخرین Tag پایدار، صفحه Releases مربوط به gqlgen را بررسی کنید.

go install github.com/99designs/gqlgen@v0.17.49

سپس، دایرکتوری پروژه‌ای با نام parmin برای ذخیره فایل‌های این پروژه بسازید:

mkdir parmin

وارد دایرکتوری پروژه parmin شوید:

cd parmin

از دایرکتوری پروژه خود، دستور زیر را برای ساخت یک فایل go.mod که ماژول‌های داخل پروژه parmin را مدیریت می‌کند، اجرا کنید:

go mod init parmin

سپس، با nano یا ویرایشگر متن دلخواه خود، فایلی با نام tools.go داخل دایرکتوری پروژه بسازید:

nano tools.go

خطوط زیر را در فایل tools.go به‌عنوان یک ابزار برای پروژه اضافه کنید:

tools.go

//go:build tools

package tools

import _ "github.com/99designs/gqlgen"

سپس، دستور tidy را برای نصب وابستگی gqlgen که در فایل tools.go معرفی شد اجرا کنید:

go mod tidy

در نهایت، با استفاده از کتابخانه Gqlgen نصب‌شده، فایل‌های Boilerplate لازم برای GraphQL API را تولید کنید:

gqlgen init

اجرای gqlgen init یک فایل server.go برای اجرای سرور GraphQL و یک دایرکتوری graph حاوی فایل schema.graphqls با تعاریف Schema مربوط به GraphQL API تولید می‌کند.

توجه: اگر پیام gqlgen: command not found را دیدید، یعنی مسیر باینری Go در PATH شل شما نیست. آن را با این دستور اضافه کنید:

export PATH=$PATH:$(go env GOPATH)/bin

این خط را به ‎~/.bashrc یا ‎~/.zshrc خود اضافه کنید تا دائمی شود.

در این مرحله، از کتابخانه Gqlgen برای راه‌اندازی اولیه GraphQL API استفاده کردید. در ادامه، Schema اپلیکیشن GraphQL را تعریف می‌کنید.

مرحله ۲ — تعریف Schema اپلیکیشن GraphQL

در این مرحله، Schema اپلیکیشن GraphQL را با تغییر فایل schema.graphqls — که هنگام اجرای دستور gqlgen init به‌طور خودکار تولید شد — تعریف می‌کنید. در این فایل، انواع User و Query و Mutation را تعریف می‌کنید.

به دایرکتوری graph رفته و فایل schema.graphqls را باز کنید؛ فایلی که Schema اپلیکیشن GraphQL را تعریف می‌کند. Schema Boilerplate را با بلوک کد زیر جایگزین کنید که نوع User را همراه با یک Query برای دریافت همه داده‌های کاربر و یک Mutation برای درج داده تعریف می‌کند:

schema.graphqls

scalar Upload

type User {
  id: ID!
  fullName: String!
  email: String!
  img_uri: String!
  DateCreated: String!
}

type Query {
  users: [User]!
}

input NewUser {
  fullName: String!
  email: String!
  img_uri: String
  DateCreated: String
}

input ProfileImage {
  userId: String
  file: Upload
}

type Mutation {
  createUser(input: NewUser!): User!
  uploadProfileImage(input: ProfileImage!): Boolean!
}

بلوک کد، دو نوع Mutation و یک نوع Query منفرد برای دریافت همه کاربران تعریف می‌کند. یک Mutation برای درج یا تغییر داده‌های موجود در یک اپلیکیشن GraphQL استفاده می‌شود؛ درحالی‌که یک Query برای واکشی داده‌هاست؛ مشابه فعل HTTP با نام GET در یک REST API.

Schema در بلوک کد بالا از زبان تعریف Schema گراف‌کیوال برای تعریف یک Mutation حاوی نوع CreateUser استفاده کرد که ورودی NewUser را به‌عنوان پارامتر می‌پذیرد و یک کاربر منفرد برمی‌گرداند. همچنین شامل نوع uploadProfileImage است که ProfileImage را می‌پذیرد و یک مقدار Boolean برای نشان‌دادن وضعیت موفقیت عملیات آپلود برمی‌گرداند.

توجه: Gqlgen به‌طور خودکار نوع Scalar با نام Upload را تعریف کرده و مشخصات یک فایل را تعریف می‌کند. برای استفاده از آن، فقط باید آن را در بالای فایل Schema — همان‌طور که در بلوک کد بالا انجام شد — اعلام کنید.

در این نقطه، ساختار مدل داده اپلیکیشن را تعریف کرده‌اید. قدم بعدی، تولید توابع Resolver مربوط به Query و Mutation این Schema با استفاده از قابلیت تولید کد Gqlgen است.

مرحله ۳ — تولید Resolverهای اپلیکیشن

در این مرحله، از قابلیت تولید کد Gqlgen استفاده می‌کنید تا Resolverهای GraphQL را بر اساس Schemaای که در مرحله قبل ساختید، به‌طور خودکار تولید کنید. یک Resolver تابعی است که برای یک فیلد GraphQL مقداری را Resolve یا برمی‌گرداند. این مقدار می‌تواند یک آبجکت یا یک نوع Scalar — مانند رشته، عدد یا حتی Boolean — باشد.

پکیج Gqlgen بر رویکرد Schema-First بنا شده است. یک قابلیت وقت‌سازِ Gqlgen، توانایی تولید Resolverهای اپلیکیشن شما بر اساس Schema تعریف‌شده در فایل schema.graphqls است. با این قابلیت، نیازی به نوشتن دستی کد Boilerplate مربوط به Resolverها ندارید؛ یعنی می‌توانید روی پیاده‌سازی Resolverهای تعریف‌شده تمرکز کنید.

برای استفاده از قابلیت تولید کد، دستور زیر را در دایرکتوری پروژه اجرا کنید تا فایل‌های مدل و Resolverهای GraphQL API تولید شوند:

gqlgen generate

پس از اجرای دستور gqlgen generate، دو خطای اعتبارسنجی در ترمینال چاپ می‌شوند. این خطاها به این دلیل رخ می‌دهند که gqlgen Resolverهای Stub قدیمی (CreateTodo و Todos) را از قالب Schema پیش‌فرض Todo خود با خود آورده که دیگر با Schemaای که تعریف کردید مطابقت ندارند. خروجی مشابه این خواهد بود:

خروجی:

# parmin/graph
graph/schema.resolvers.go:22:32: undefined: model.NewTodo
graph/schema.resolvers.go:27:55: undefined: model.Todo

این خطاها را با حذف Stubهای قدیمی در مرحله بعد رفع می‌کنید. فایل‌های جدیدی هم تولید شده و پروژه شما ساختار پوشه جدیدی خواهد داشت.

دستور tree را از ریشه پروژه خود اجرا کنید تا فایل‌های تولیدشده را ببینید:

tree

ساختار دایرکتوری فعلی به این شکل خواهد بود:

خروجی:

go.mod
go.sum
gqlgen.yml
graph
  ├── db.go
  ├── generated
  │   └── generated.go
  ├── model
  │   └── models_gen.go
  ├── resolver.go
  ├── schema.graphqls
  └── schema.resolvers.go
server.go
tmp
  ├── build-errors.log
  └── main
tools.go

2 directories, 8 files

در میان فایل‌های پروژه، یک فایل مهم schema.resolvers.go است. این فایل متدهایی را در بر دارد که انواع Mutation و Query را که قبلاً در فایل schema.graphqls تعریف شدند، پیاده‌سازی می‌کنند.

برای رفع خطاهای اعتبارسنجی، متدهای CreateTodo و Todos را در انتهای فایل schema.resolvers.go حذف کنید. Gqlgen این متدها را به انتهای فایل منتقل کرده؛ چون تعاریف نوع در فایل schema.graphqls تغییر کرده بودند.

schema.resolvers.go

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.

import (
    "context"
    "parmin/graph/generated"
    "parmin/graph/model"
    "fmt"
)

func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
    panic(fmt.Errorf("not implemented"))
}

func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
    panic(fmt.Errorf("not implemented"))
}

func (r *queryResolver) User(ctx context.Context) (*model.User, error) {
    panic(fmt.Errorf("not implemented"))
}

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
//  - When renaming or deleting a resolver the old code will be put in here. You can safely delete
//    it when you're done.
//  - You have helper methods in this file. Move them out to keep these resolver files clean.

func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
 panic(fmt.Errorf("not implemented"))
}
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
 panic(fmt.Errorf("not implemented"))
}

طبق تعریف فایل schema.graphqls، تولیدکننده کد Gqlgen دو Mutation و یک متد Resolver مربوط به Query ساخت. این Resolverها این اهداف را خدمت می‌کنند:

  • CreateUser: این Mutation Resolver، یک رکورد کاربر جدید را در پایگاه داده Postgres متصل‌شده درج می‌کند.
  • UploadProfileImage: این Mutation Resolver، یک فایل رسانه‌ای دریافت‌شده از یک درخواست Multipart HTTP را گرفته و فایل را به یک Bucket داخل Spaces پارمین کلود آپلود می‌کند. پس از آپلود فایل، URL فایل آپلودشده در فیلد img_uri کاربرِ قبلاً ایجادشده درج می‌شود.
  • Users: این Query Resolver، پایگاه داده را برای همه کاربران موجود کوئری کرده و آن‌ها را به‌عنوان نتیجه کوئری برمی‌گرداند.

با مرور متدهای تولیدشده از انواع Mutation و Query، متوجه می‌شوید که هنگام اجرا باعث panic با خطای not implemented می‌شوند. این نشان می‌دهد که هنوز کد Boilerplate تولیدشده خودکارند. بعداً در این آموزش، به فایل schema.resolvers.go برمی‌گردید تا این متدهای تولیدشده را پیاده‌سازی کنید.

در این نقطه، Resolverهای این اپلیکیشن را بر اساس محتوای فایل schema.graphqls تولید کردید. اکنون از سرویس پایگاه داده مدیریت‌شده برای ساخت پایگاه داده‌ای استفاده می‌کنید که داده‌های پاس‌داده‌شده به Mutation Resolverها برای ساخت کاربر را ذخیره کند.

مرحله ۴ — آماده‌سازی و استفاده از یک اینستنس پایگاه داده مدیریت‌شده در پارمین کلود

در این مرحله، از کنسول پارمین کلود برای دسترسی به سرویس پایگاه داده مدیریت‌شده و ساخت یک پایگاه داده PostgreSQL برای ذخیره داده‌های این اپلیکیشن استفاده می‌کنید. پس از ساخته‌شدن پایگاه داده، جزئیات را به‌صورت امن در یک فایل .env ذخیره می‌کنید.

اگرچه اپلیکیشن تصاویر را مستقیماً در پایگاه داده ذخیره نمی‌کند، همچنان به پایگاه داده‌ای برای درج رکورد هر کاربر نیاز دارد. رکورد ذخیره‌شده سپس حاوی لینک‌هایی به فایل‌های آپلودشده خواهد بود.

رکورد یک کاربر شامل Fullname، email، dateCreated و یک فیلد img_uri از نوع داده String خواهد بود. فیلد img_uri حاوی URLای است که به یک فایل تصویری اشاره می‌کند که توسط یک کاربر از طریق این GraphQL API آپلود شده و در یک Bucket روی Spaces پارمین کلود ذخیره شده است.

با استفاده از داشبورد پارمین کلود خود، به بخش Databases کنسول بروید تا یک کلاستر پایگاه داده جدید بسازید و PostgreSQL را از فهرست پایگاه‌های داده عرضه‌شده انتخاب کنید. همه سایر تنظیمات را روی مقادیر پیش‌فرضشان بگذارید و این کلاستر را با دکمه پایین صفحه بسازید.

[تصویر: کلاستر پایگاه داده در پارمین کلود]

فرایند ساخت کلاستر پایگاه داده چند دقیقه طول می‌کشد تا کامل شود.

پس از ساخت کلاستر، مراحل «Getting Started» را در صفحه کلاستر پایگاه داده دنبال کنید تا کلاستر برای استفاده آماده شود.

در مرحله دوم راهنمای Getting Started، برای ادامه روی متن «Continue, I’ll do this later» کلیک کنید. به‌صورت پیش‌فرض، کلاستر پایگاه داده به همه اتصالات باز است.

توجه: در یک سناریوی آماده production، فیلد ورودی «Add Trusted Sources» در مرحله دوم باید فقط IPهای مورد اعتماد — مانند IP سرور مجازی پارمین کلود که اپلیکیشن را اجرا می‌کند — را در بر داشته باشد. در طول توسعه، می‌توانید به‌جای آن IP ماشین توسعه خود را به فیلد ورودی Add Trusted Sources اضافه کنید.

روی دکمه «Allow these inbound sources» کلیک کنید تا ذخیره شده و به مرحله بعد بروید.

در مرحله بعد، جزئیات اتصال کلاستر نمایش داده می‌شوند. همچنین می‌توانید اطلاعات احراز هویت کلاستر را با کلیک روی منوی کشویی Actions و سپس انتخاب گزینه Connection details پیدا کنید.

[تصویر: اطلاعات اتصال کلاستر پایگاه داده در پارمین کلود]

در این اسکرین‌شات، کادر خاکستری سمت راست، اطلاعات احراز هویت اتصال کلاستر دموی ساخته‌شده را نشان می‌دهد.

این اطلاعات احراز هویت کلاستر را به‌صورت امن به‌عنوان متغیرهای محیطی ذخیره می‌کنید. در دایرکتوری پروژه parmin، یک فایل .env بسازید و اطلاعات احراز هویت کلاستر خود را به این قالب اضافه کنید؛ حتماً محتوای Placeholder هایلایت‌شده را با اطلاعات خودتان جایگزین کنید:

.env

 DB_PASSWORD=YOUR_DB_PASSWORD
 DB_PORT=PORT
 DB_NAME=YOUR_DATABASE_NAME
 DB_ADDR=HOST
 DB_USER=USERNAME

با ذخیره‌شدن امن جزئیات اتصال در فایل .env، قدم بعدی بازیابی این اطلاعات احراز هویت و اتصال کلاستر پایگاه داده به پروژه شماست.

قبل از ادامه، برای کار با پکیج SQL بومی Golang هنگام اتصال به پایگاه داده Postgres، به یک درایور پایگاه داده نیاز دارید. go-pg یک کتابخانه Golang برای ترجمه کوئری‌های ORM (نگاشت شیء-رابطه‌ای) به کوئری‌های SQL برای پایگاه داده Postgres است. godotenv یک کتابخانه Golang برای بارگذاری اطلاعات احراز هویت محیطی از یک فایل .env به اپلیکیشن شماست. در نهایت، go.uuid برای هر رکورد کاربری که در پایگاه داده درج می‌شود، یک UUID (شناسه یکتای جهانی) تولید می‌کند.

این دستور را برای نصب این موارد اجرا کنید:

go get github.com/go-pg/pg/v10 github.com/joho/godotenv github.com/satori/go.uuid

توجه: این آموزش از go-pg نسخه v10 استفاده می‌کند که همچنان با نسخه‌های فعلی Go سازگار است. برای پروژه‌های جدید، ORM با نام bun از همان نگهدارنده را در نظر بگیرید.

سپس، به دایرکتوری graph رفته و یک فایل db.go بسازید. کد داخل این فایل را به‌تدریج برای اتصال به پایگاه داده Postgresِ ساخته‌شده در کلاستر پایگاه داده مدیریت‌شده، کنار هم می‌گذارید.

ابتدا، محتوای بلوک کد را به فایل db.go اضافه کنید. این تابع (createSchema) بلافاصله پس از برقراری اتصال به پایگاه داده، یک جدول کاربر در پایگاه داده Postgres می‌سازد.

db.go

package graph

import (
    "github.com/go-pg/pg/v10"
    "github.com/go-pg/pg/v10/orm"
    "parmin/graph/model"
)

func createSchema(db *pg.DB) error {
    for _, models := range []interface{}{(*model.User)(nil)}{
        if err := db.Model(models).CreateTable(&orm.CreateTableOptions{
            IfNotExists: true,
        }); err != nil {
            panic(err)
        }
    }

    return nil
}

با استفاده از گزینه IfNotExists که به متد CreateTable از go-pg پاس می‌شود، تابع createSchema فقط در صورتی جدول جدیدی را در پایگاه داده درج می‌کند که جدول وجود نداشته باشد. می‌توانید این فرایند را شکل ساده‌شده‌ای از Seed کردن یک پایگاه داده تازه‌ساخته درک کنید. به‌جای ساخت دستی جدول‌ها از طریق کلاینت psql یا رابط گرافیکی، تابع createSchema کار ساخت جدول را انجام می‌دهد.

سپس، محتوای بلوک کد زیر را به فایل db.go اضافه کنید تا یک اتصال به پایگاه داده Postgres برقرار شده و تابع createConfig بالا پس از برقراری موفق اتصال اجرا شود:

db.go

import (
      // ...

         "fmt" 
         "os" 
    )

func Connect() *pg.DB {
    DB_PASSWORD := os.Getenv("DB_PASSWORD")
    DB_PORT := os.Getenv("DB_PORT")
    DB_NAME := os.Getenv("DB_NAME")
    DB_ADDR := os.Getenv("DB_ADDR")
    DB_USER := os.Getenv("DB_USER")

    connStr := fmt.Sprintf(
        "postgresql://%v:%v@%v:%v/%v?sslmode=require",
        DB_USER, DB_PASSWORD, DB_ADDR, DB_PORT, DB_NAME )

    opt, err := pg.ParseURL(connStr); if err != nil {
      panic(err)
      }

    db := pg.Connect(opt)

    if schemaErr := createSchema(db); schemaErr != nil {
        panic(schemaErr)
    }

    if _, DBStatus := db.Exec("SELECT 1"); DBStatus != nil {
        panic("PostgreSQL is down")
    }

    return db
}

هنگام اجرا، تابع Export شده Connect در بلوک کد بالا با استفاده از go-pg یک اتصال به پایگاه داده Postgres برقرار می‌کند. این کار از طریق عملیات زیر انجام می‌شود:

ابتدا، اطلاعات احراز هویت پایگاه داده‌ای که در فایل .env ریشه ذخیره کرده‌اید بازیابی می‌شوند. سپس، متغیری ساخته می‌شود که رشته‌ای قالب‌بندی‌شده با اطلاعات احراز هویت بازیابی‌شده را ذخیره کند. این متغیر هنگام اتصال به پایگاه داده به‌عنوان URI اتصال استفاده خواهد شد.

سپس، رشته اتصال ساخته‌شده تجزیه می‌شود تا ببینید اطلاعات قالب‌بندی‌شده معتبرند یا نه. در صورت معتبر بودن، رشته اتصال به متد connect به‌عنوان آرگومان پاس داده شده تا اتصالی برقرار شود.

برای استفاده از تابع Export شده Connect، باید تابع را به فایل server.go اضافه کنید تا هنگام شروع اپلیکیشن اجرا شود. سپس اتصال را می‌توان در فیلد DB داخل ساختار Resolver ذخیره کرد.

برای استفاده از تابع Connect که قبلاً از پکیج graph ساخته شد — بلافاصله پس از شروع اپلیکیشن — و برای بارگذاری اطلاعات احراز هویت از فایل .env به اپلیکیشن، فایل server.go را در ویرایشگر کد دلخواه خود باز کرده و خطوط هایلایت‌شده زیر را اضافه کنید:

توجه: مطمئن شوید متغیر srv موجود در فایل server.go را با متغیر srv هایلایت‌شده زیر جایگزین می‌کنید.

server.go

 package main

import (
  "log"
  "net/http"
  "os"
  "parmin/graph"
  "parmin/graph/generated"

  "github.com/99designs/gqlgen/graphql/handler"
  "github.com/99designs/gqlgen/graphql/playground"
 "github.com/joho/godotenv"
)

const defaultPort = "8080"

func main() {
     err := godotenv.Load(); if err != nil {
     log.Fatal("Error loading .env file")
    } 

  // ...

     Database := graph.Connect()
     srv := handler.NewDefaultServer(
             generated.NewExecutableSchema(
                     generated.Config{
                         Resolvers: &graph.Resolver{
                             DB: Database,
                         },
                     }),
         )

  // ...
}

در این قطعه کد، اطلاعات احراز هویت ذخیره‌شده در .env را از طریق تابع ()Load بارگذاری کردید. تابع Connect را از پکیج db فراخوانی کرده و آبجکت Resolver را هم با اتصال پایگاه داده ذخیره‌شده در فیلد DB ساختید. (اتصال پایگاه داده ذخیره‌شده بعداً در این آموزش توسط Resolverها قابل دسترسی خواهد بود.)

در حال حاضر، ساختار Resolver مربوط به Boilerplate در فایل resolver.go، فیلد DBای را که اتصال پایگاه داده را در کد بالا ذخیره کردید، در بر ندارد. باید فیلد DB را بسازید.

در دایرکتوری graph، فایل resolver.go را باز کرده و ساختار Resolver را طوری تغییر دهید که فیلدی DB با نوع Pointer مربوط به go-pg داشته باشد؛ همان‌طور که زیر نشان داده شده:

resolver.go

package graph

import "github.com/go-pg/pg/v10"

// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.

type Resolver struct {
    DB *pg.DB
}

اکنون هر بار که فایل ورودی server.go اجرا شود، یک اتصال پایگاه داده برقرار می‌شود و پکیج go-pg می‌تواند به‌عنوان ORM از توابع Resolver روی پایگاه داده عملیات انجام دهد.

در این مرحله، با استفاده از سرویس پایگاه داده مدیریت‌شده در پارمین کلود، یک پایگاه داده PostgreSQL ساختید. همچنین فایل db.go با تابع Connect را ساختید تا هنگام شروع اپلیکیشن، اتصالی به پایگاه داده PostgreSQL برقرار شود. در ادامه، Resolverهای تولیدشده را برای ذخیره داده‌ها در پایگاه داده PostgreSQL پیاده‌سازی می‌کنید.

مرحله ۵ — پیاده‌سازی Resolverهای تولیدشده

در این مرحله، متدهای موجود در فایل schema.resolvers.go را پیاده‌سازی می‌کنید؛ فایلی که به‌عنوان Resolverهای Mutation و Query عمل می‌کند. Mutation Resolverهای پیاده‌سازی‌شده، یک کاربر می‌سازند و تصویر پروفایل کاربر را آپلود می‌کنند؛ درحالی‌که Query Resolver همه جزئیات کاربران ذخیره‌شده را بازیابی می‌کند.

پیاده‌سازی متدهای Mutation Resolver

در فایل schema.graphqls، دو Mutation Resolver تولید شدند. یکی با هدف درج رکورد کاربر و دیگری برای هندل آپلود تصاویر پروفایل. با این حال، این Mutationها هنوز پیاده‌سازی نشده‌اند؛ چون کد Boilerplate هستند.

فایل schema.resolvers.go را باز کنید. ایمپورت‌ها و Mutation مربوط به CreateUser را با خطوط هایلایت‌شده تغییر دهید تا سطر جدیدی حاوی ورودی جزئیات کاربر در پایگاه داده درج شود:

schema.resolvers.go

package graph

import (
  "context"
  "fmt"
   "time"

  "parmin/graph/generated"
  "parmin/graph/model"
  "github.com/satori/go.uuid"
)

func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser) (*model.User, error) {
     user := model.User{
         ID:          fmt.Sprintf("%v", uuid.NewV4()),
         FullName:    input.FullName,
         Email:       input.Email,
         ImgURI:      "https://bit.ly/3mCSn2i",
         DateCreated: time.Now().Format("01-02-2006"),
     }

     _, err := r.DB.Model(&user).Insert(); if err != nil {
         return nil, fmt.Errorf("error inserting user: %v", err)
     }

     return &user, nil
}

در Mutation مربوط به CreateUser، دو نکته درباره سطرهای کاربرِ درج‌شده وجود دارد. اول اینکه به هر سطر درج‌شده یک UUID داده می‌شود. دوم، فیلد ImgURI در هر سطر یک URL تصویر Placeholder به‌عنوان مقدار پیش‌فرض دارد. این مقدار پیش‌فرضِ همه رکوردها خواهد بود و وقتی کاربر تصویر جدیدی آپلود کند، به‌روزرسانی می‌شود.

سپس، اپلیکیشنی که تا این نقطه ساخته شده را تست می‌کنید. از دایرکتوری پروژه، فایل server.go را با دستور زیر اجرا کنید:

go run ./server.go

حالا، از طریق مرورگر وب خود به http://localhost:8080 بروید تا به GraphQL Playground داخلیِ GraphQL API خود دسترسی پیدا کنید. Mutation گراف‌کیوالِ بلوک کد زیر را در ویرایشگر Playground جای‌گذاری کنید تا یک رکورد کاربر جدید درج شود:

mutation createUser {
  createUser(
    input: {
      email: "johndoe@gmail.com"
      fullName: "John Doe"
    }
  ) {
    id
  }
}

خروجی در پنل سمت راست مشابه این خواهد بود:

[تصویر: اجرای Mutation ساخت کاربر در GraphQL Playground]

شما Mutation مربوط به CreateUser را برای ساخت یک کاربر تستی با نام John Doe اجرا کردید و id رکورد کاربر تازه درج‌شده به‌عنوان نتیجه Mutation برگردانده شد.

توجه: مقدار id برگردانده‌شده از کوئری گراف‌کیوال اجراشده را کپی کنید. از این id هنگام آپلود تصویر پروفایل برای کاربر تستیِ بالا استفاده خواهید کرد.

در این نقطه، تابع Mutation Resolver دوم یعنی UploadProfileImage برای پیاده‌سازی باقی مانده است. اما قبل از پیاده‌سازی این تابع، باید Query Resolver را پیاده‌سازی کنید. دلیلش این است که هر آپلود به یک کاربر مشخص لینک می‌شود؛ و به همین دلیل ID یک کاربر مشخص را قبل از آپلود تصویر بازیابی کردید.

پیاده‌سازی متد Query Resolver

طبق تعریف فایل schema.graphqls، یک Query Resolver برای بازیابی همه کاربرانِ ساخته‌شده تولید شد. مشابه متدهای Mutation Resolver قبلی، باید متد Query Resolver را هم پیاده‌سازی کنید.

فایل schema.resolvers.go را باز کرده و Query Resolver تولیدشده Users را با خطوط هایلایت‌شده تغییر دهید. کد جدید داخل متد Users پایین، پایگاه داده Postgres را برای همه سطرهای کاربر کوئری کرده و نتیجه را برمی‌گرداند.

schema.resolvers.go

package graph

func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
  var users []*model.User

  err := r.DB.Model(&users).Select()
    if err != nil {
     return nil, err
    }

  return users, nil
}

داخل تابع Resolver مربوط به Users در بالا، واکشی همه رکوردهای داخل جدول کاربر با استفاده از متد select مربوط به go-pg روی مدل User — بدون پاس دادن بند WHERE یا LIMIT به کوئری — ممکن شده است.

توجه: برای یک اپلیکیشن بزرگ‌تر که رکوردهای زیادی از کوئری برگردانده می‌شوند، در نظر گرفتن صفحه‌بندی (Pagination) داده‌های برگردانده‌شده برای کارایی بهتر مهم است.

برای تست این Query Resolver از مرورگر خود، به http://localhost:8080 بروید تا به GraphQL Playground دسترسی پیدا کنید. Query گراف‌کیوال زیر را در ویرایشگر Playground جای‌گذاری کنید تا همه رکوردهای کاربرانِ ساخته‌شده واکشی شوند:

query fetchUsers {
  users {
      fullName
      id
      img_uri
  }
}

خروجی در پنل سمت راست مشابه این خواهد بود:

[تصویر: نتیجه Query در GraphQL Playground]

در نتایج برگردانده‌شده می‌بینید که یک آبجکت users با مقدار آرایه‌ای برگردانده شد. فعلاً فقط کاربرِ قبلاً ساخته‌شده در آرایه users برگردانده شده؛ چون تنها رکورد جدول همین است. اگر Mutation مربوط به createUser را با جزئیات جدید اجرا کنید، کاربران بیشتری در آرایه users برگردانده می‌شوند. همچنین می‌توانید ببینید که فیلد img_uri در داده‌های برگردانده‌شده، URL تصویرِ جایگزینِ هاردکد‌شده را دارد.

در این نقطه، هر دو موردِ Mutation مربوط به CreateUser و Query مربوط به User را پیاده‌سازی کرده‌اید. همه‌چیز آماده است تا تصاویر را از Resolver دوم یعنی UploadProfileImage دریافت کرده و تصویر دریافتی را با یک SDK سازگار با S3 از AWS-GO به یک Bucket در Spaces پارمین کلود آپلود کنید.

مرحله ۶ — آپلود تصاویر به Spaces پارمین کلود

در این مرحله، Mutation Resolver مربوط به UploadProfileImage را پیاده‌سازی می‌کنید تا با استفاده از AWS SDK برای Go v2، یک تصویر را به Space پارمین کلود خود آپلود کند.

برای شروع، به بخش Spaces کنسول پارمین کلود خود بروید؛ جایی که یک Bucket جدید برای ذخیره فایل‌های آپلودشده از اپلیکیشن Backend خود می‌سازید.

روی دکمه «Create New Space» کلیک کنید. تنظیمات را روی مقادیر پیش‌فرض بگذارید و یک نام یکتا برای Space جدید مشخص کنید:

[تصویر: ساخت Space جدید در پارمین کلود]

پس از ساخته‌شدن Space جدید، به تب settings بروید و Endpoint، نام و Region مربوط به Space را کپی کنید. این موارد را به فایل .env داخل پروژه GraphQL به این قالب اضافه کنید:

.env

SPACE_ENDPOINT=BUCKET_ENDPOINT
DO_SPACE_REGION=DO_SPACE_REGION
DO_SPACE_NAME=DO_SPACE_NAME

به‌عنوان مثال، اسکرین‌شات زیر تب Settings را نشان می‌دهد و جزئیات نام، Region و Endpoint مربوط به Space نمونه (‏Victory-space) را هایلایت می‌کند:

[تصویر: Endpoint، نام و Region مربوط به Space نمونه (Victory-space)]

به‌عنوان بخشی از پیش‌نیازها، یک Access Key و Secret Key برای Space خود ساختید. Access Key و Secret Key خود را در فایل .env داخل اپلیکیشن GraphQL به این قالب جای‌گذاری کنید:

.env

ACCESS_KEY=YOUR_SPACE_ACCESS_KEY
SECRET_KEY=YOUR_SPACE_SECRET_KEY

در این نقطه، باید با ترکیب کلیدهای CTRL + C سرور GraphQL را متوقف کرده و دستور زیر را برای ری‌استارت اپلیکیشن GraphQL با اطلاعات احراز هویت جدیدِ بارگذاری‌شده در اپلیکیشن اجرا کنید:

go run ./server.go

حالا که اطلاعات احراز هویت Space شما در اپلیکیشن بارگذاری شده، منطق آپلود را در Mutation Resolver مربوط به UploadProfileImage می‌سازید. اولین قدم، افزودن و پیکربندی AWS SDK برای Go v2 برای اتصال به Space پارمین کلود شماست. مستندات Spaces پارمین کلود، عملیاتی را که می‌توانید با یک AWS SDK روی API مربوط به Spaces انجام دهید فهرست می‌کند.

دستور زیر را برای نصب پکیج‌های AWS SDK Go v2 اجرا کنید:

go get github.com/aws/aws-sdk-go-v2 github.com/aws/aws-sdk-go-v2/config github.com/aws/aws-sdk-go-v2/credentials github.com/aws/aws-sdk-go-v2/service/s3

در چند بلوک کد بعدی، منطق آپلود را در Mutation Resolver مربوط به UploadProfileImage به‌تدریج کنار هم می‌گذارید.

ابتدا، فایل schema.resolvers.go را باز کنید. خطوط هایلایت‌شده را برای پیکربندی AWS SDK با اطلاعات احراز هویت ذخیره‌شده و برقراری اتصال با Space پارمین کلود خود اضافه کنید:

توجه: کد داخل بلوک کد زیر کامل نیست؛ چون در حال کنار هم گذاشتن تدریجی منطق آپلود هستید. کد را در بلوک‌های بعدی کامل می‌کنید.

schema.resolvers.go

package graph

import (
    "context"
    "fmt"
    "os"
    ...

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/credentials"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
    cfg, err := config.LoadDefaultConfig(ctx,
        config.WithRegion(os.Getenv("DO_SPACE_REGION")),
        config.WithCredentialsProvider(
            credentials.NewStaticCredentialsProvider(
                os.Getenv("ACCESS_KEY"),
                os.Getenv("SECRET_KEY"),
                "",
            ),
        ),
        config.WithBaseEndpoint(os.Getenv("SPACE_ENDPOINT")),
    )
    if err != nil {
        return false, fmt.Errorf("failed to load AWS config: %w", err)
    }
    s3Client := s3.NewFromConfig(cfg)
    // ... (upload logic in next block)
}

حالا که SDK پیکربندی شد، قدم بعدی آپلود فایلِ ارسالی در درخواست Multipart HTTP است.

محتوای فایل را با io.ReadAll از درخواست Multipart بخوانید؛ سپس آن را با API مربوط به PutObject از AWS SDK Go v2 به Spaces آپلود کنید. کلاینت از همان Endpoint گراف‌کیوال برای آپلود فایل‌ها استفاده می‌کند؛ پس هیچ API آپلود جداگانه‌ای لازم نیست.

برای این کار، کد زیر را به Mutation Resolver موجود UploadProfileImage در فایل schema.resolvers.go اضافه کنید. ایمپورت io و پکیج types مربوط به s3 را اضافه کنید:

schema.resolvers.go

package graph

import (
    "bytes"
    "context"
    "fmt"
    "io"
    "os"
    ...

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/service/s3"
    "github.com/aws/aws-sdk-go-v2/service/s3/types"
)

func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
    // ... (cfg and s3Client from previous block)

    SpaceName := os.Getenv("DO_SPACE_NAME")
    SpaceRegion := os.Getenv("DO_SPACE_REGION")
    userFileName := fmt.Sprintf("%v-%v", input.UserID, input.File.Filename)

    fileBytes, readErr := io.ReadAll(input.File.File)
    if readErr != nil {
        return false, fmt.Errorf("error reading file: %w", readErr)
    }

    _, uploadErr := s3Client.PutObject(ctx, &s3.PutObjectInput{
        Bucket: aws.String(SpaceName),
        Key:    aws.String(userFileName),
        Body:   bytes.NewReader(fileBytes),
        ACL:    types.ObjectCannedACLPublicRead,
    })
    if uploadErr != nil {
        return false, fmt.Errorf("error uploading file: %w", uploadErr)
    }

    // ... (user update logic in next block)
    return true, nil
}

توجه: متغیر SpaceRegion اینجا اعلام شده اما در بلوک کد بعدی برای ساخت URL فایل آپلودشده استفاده می‌شود. Go اگر سرور را قبل از افزودن منطق به‌روزرسانی کاربر در مرحله بعد اجرا کنید، خطای «declared and not used» برمی‌گرداند.

با استفاده از تابع ReadAll از پکیج io، محتوای فایل را از درخواست Multipart در حافظه می‌خوانید؛ سپس آن را از طریق bytes.NewReader به PutObject پاس می‌دهید. برای فایل‌های خیلی بزرگ، می‌توانستید مستقیماً از input.File.File استریم کنید تا از بارگذاری کل فایل در حافظه اجتناب شود. ساختار PutObjectInput موارد Bucket، Key، ACL و Body را برای آپلود مشخص می‌کند.

توجه: فیلد Access Control List (ACL) روی types.ObjectCannedACLPublicRead تنظیم شده تا فایل‌های آپلودشده روی اینترنت قابل مشاهده باشند. اگر اپلیکیشن شما آبجکت‌های خصوصی می‌خواهد، این فیلد را حذف کنید.

متد PutObject آپلود به Bucket را انجام می‌دهد. در صورت خطا، Resolver مقدار false و یک خطا برمی‌گرداند و Mutation پایان می‌یابد.

برای تست Mutation Resolver آپلود، می‌توانید از یک تصویر نمونه استفاده کنید. با دستور wget تصویر نمونه را دانلود کنید:

wget https://html.sammy-codes.com/images/small-profile.jpeg

سپس، دستور cURL زیر را برای ارسال یک درخواست HTTP به GraphQL API و آپلود تصویر — که به بدنه فرم درخواست اضافه شده — اجرا کنید:

توجه: اگر روی سیستم‌عامل ویندوز هستید، به دلیل Escapeهای Backslash، توصیه می‌شود دستورات cURL را با شل Git Bash اجرا کنید.

curl localhost:8080/query  -F operations='{ "query": "mutation uploadProfileImage($image: Upload! $userId : String!) { uploadProfileImage(input: { file: $image  userId : $userId}) }", "variables": { "image": null, "userId" : "12345" } }' -F map='{ "0": ["variables.image"] }'  -F 0=@small-profile.jpeg

توجه: در درخواست بالا از یک مقدار userId تصادفی استفاده می‌کنیم؛ چون فرایند به‌روزرسانی رکورد کاربر هنوز پیاده‌سازی نشده است.

خروجی مشابه این خواهد بود که نشان می‌دهد آپلود فایل موفق بوده:

{"data": { "uploadProfileImage": true }}

در بخش Spaces کنسول پارمین کلود، تصویرِ آپلودشده از ترمینال خود را پیدا می‌کنید:

[تصویر: یک Bucket در پارمین کلود با فهرست فایل‌های آپلودشده]

در این نقطه، آپلود فایل داخل اپلیکیشن کار می‌کند؛ اما فایل‌ها به کاربری که آپلود را انجام داده لینک نیستند. هدف هر آپلود فایل این است که فایل در یک Bucket ذخیره‌سازی آپلود شده و سپس با به‌روزرسانی فیلد img_uri کاربر، به او لینک شود.

فایل resolver.go را در دایرکتوری graph باز کرده و بلوک کد زیر را اضافه کنید. این فایل شامل دو متد است: یکی برای بازیابی یک کاربر از پایگاه داده بر اساس یک فیلد مشخص و تابع دیگری برای به‌روزرسانی رکورد یک کاربر.

resolver.go

import (
...

  "parmin/graph/model"
  "fmt"
)

...

func (r *mutationResolver) GetUserByField(field, value string) (*model.User, error) {
    user := model.User{}

    err := r.DB.Model(&user).Where(fmt.Sprintf("%v = ?", field), value).First()

    return &user, err
}

func (r *mutationResolver) UpdateUser(user *model.User) (*model.User, error) {
    _, err := r.DB.Model(user).Where("id = ?", user.ID).Update()
    return user, err
}

اولین تابع GetUserByField بالا، آرگومان‌های field و value — هر دو از نوع رشته — را می‌پذیرد. با استفاده از ORM مربوط به go-pg، یک کوئری روی پایگاه داده اجرا کرده و با یک بند WHERE از جدول کاربر داده واکشی می‌کند.

دومین تابع UpdateUser در بلوک کد، با استفاده از go-pg یک عبارت UPDATE برای به‌روزرسانی یک رکورد در جدول کاربر اجرا می‌کند. با استفاده از متد where، بندی WHERE همراه با یک شرط به عبارت UPDATE اضافه می‌شود تا فقط سطری که همان ID پاس‌داده‌شده به تابع را دارد به‌روزرسانی شود.

اکنون می‌توانید از این دو متد در Mutation مربوط به UploadProfileImage استفاده کنید. محتوای بلوک کد هایلایت‌شده زیر را به Mutation مربوط به UploadProfileImage داخل فایل schema.resolvers.go اضافه کنید. این کار یک سطر مشخص را از جدول کاربر بازیابی کرده و پس از آپلود فایل، فیلد img_uri در رکورد کاربر را به‌روزرسانی می‌کند.

توجه: کد هایلایت‌شده را در خطی بالای دستور return موجود داخل Mutation مربوط به UploadProfileImage قرار دهید.

schema.resolvers.go

package graph

func (r *mutationResolver) UploadProfileImage(ctx context.Context, input model.ProfileImage) (bool, error) {
    // ... (upload logic above)

    user, userErr := r.GetUserByField("id", *input.UserID)
    if userErr != nil {
        return false, fmt.Errorf("error getting user: %v", userErr)
    }

    fileUrl := fmt.Sprintf("https://%v.%v.digitaloceanspaces.com/%v", SpaceName, SpaceRegion, userFileName)
    user.ImgURI = fileUrl

    if _, err := r.UpdateUser(user); err != nil {
        return false, fmt.Errorf("err updating user: %v", err)
    }

    return true, nil
}

توجه: نام فیلدی که به GetUserByField پاس داده می‌شود باید با نام ستون حروف‌کوچک، همان‌طور که در PostgreSQL وجود دارد، مطابقت کند. go-pg هنگام ساخت جدول، نام فیلدهای ساختار را به حروف کوچک تبدیل می‌کند؛ پس ID به id تبدیل می‌شود.

از کد جدید اضافه‌شده به فایل schema.resolvers.go، یک رشته id و ID کاربر به تابع کمکی GetUserByField پاس داده می‌شود تا رکورد کاربری که Mutation را اجرا می‌کند بازیابی شود.

سپس متغیر جدیدی ساخته شده و مقداری می‌گیرد که رشته‌ای قالب‌بندی‌شده برای داشتن لینک فایلِ تازه آپلودشده به قالب https://BUCKET_NAME.SPACE_REGION.digitaloceanspaces.com/USER_ID-FILE_NAME است. فیلد ImgURI در مدل کاربرِ بازیابی‌شده، مقدار این رشته قالب‌بندی‌شده به‌عنوان لینک فایل آپلودشده را دوباره گرفته است.

دستور curl زیر را در ترمینال خود جای‌گذاری کرده و Placeholder هایلایت‌شده USER_ID را در دستور با userId کاربری که از طریق GraphQL Playground در مرحله قبلی ساختید جایگزین کنید. مطمئن شوید userId داخل گیومه قرار گرفته باشد تا ترمینال بتواند مقدار را به‌درستی Encode کند:

curl localhost:8080/query  -F operations='{ "query": "mutation uploadProfileImage($image: Upload! $userId : String!) { uploadProfileImage(input: { file: $image  userId : $userId}) }", "variables": { "image": null, "userId" : "USER_ID" } }' -F map='{ "0": ["variables.image"] }'  -F 0=@small-profile.jpeg

خروجی مشابه این خواهد بود:

خروجی:

{"data": { "uploadProfileImage": true }}

برای تأیید بیشترِ به‌روزرسانی img_uri کاربر، می‌توانید از Query مربوط به fetchUsers در GraphQL Playground مرورگر برای بازیابی جزئیات کاربر استفاده کنید. اگر به‌روزرسانی موفق بوده باشد، می‌بینید که URL جایگزینِ پیش‌فرض https://bit.ly/3mCSn2i در فیلد img_uri به مقدار تصویر آپلودشده به‌روزرسانی شده است.

خروجی در پنل سمت راست مشابه این خواهد بود:

[تصویر: بازیابی رکورد به‌روز شده کاربر در GraphQL Playground]

در نتایج برگردانده‌شده، img_uri در اولین آبجکت کاربرِ برگردانده‌شده از کوئری، مقداری دارد که با آپلود فایلی به یک Bucket داخل Spaces پارمین کلود مطابقت دارد. لینک در فیلد img_uri از Endpoint مربوط به Bucket، ID کاربر و در نهایت نام فایل تشکیل شده است.

برای تست دسترسیِ فایل آپلودشده که از طریق گزینه ACL تنظیم شده، می‌توانید لینک img_uri را در مرورگر خود باز کنید. به دلیل Metadata پیش‌فرض روی تصویر آپلودشده، به‌طور خودکار به‌صورت یک فایل تصویر روی کامپیوتر شما دانلود می‌شود. می‌توانید فایل را باز کرده و تصویر را ببینید.

[تصویر: نمای دانلودشده فایل آپلود شده]

تصویر در لینک img_uri همان تصویری خواهد بود که از خط فرمان آپلود شد؛ که نشان می‌دهد متدهای فایل resolver.go به‌درستی اجرا شده‌اند و کل منطق آپلود فایل در Mutation مربوط به UploadProfileImage طبق انتظار کار می‌کند.

در این مرحله، با استفاده از AWS SDK برای Go v2 از Mutation Resolver مربوط به UploadProfileImage، یک تصویر را به Space پارمین کلود آپلود کردید.

انتخاب بین آپلود سمت سرور و Presigned URLها

این آموزش از آپلود سمت سرور استفاده می‌کند: سرور Go فایل را دریافت کرده و به Spaces استریم می‌کند. منطق کلاینت ساده می‌ماند و می‌توانید فایل را روی سرور اعتبارسنجی یا Transform کنید. توافق این است که فایل از سرور شما عبور می‌کند؛ که برای فایل‌های بزرگ، تأخیر و هزینه Egress اضافه می‌کند. این رویکرد برای فایل‌های کوچک تا متوسط و مواردی که به اعتبارسنجی سمت سرور یا دسترسی کنترل‌شده نیاز دارید مناسب است.

با URLهای Presigned، سرور Go یک Presigned URL محدود به زمان از S3 تولید کرده و آن را به کلاینت برمی‌گرداند. کلاینت مستقیماً به Spaces آپلود می‌کند؛ پس سرور هرگز Payload باینری را هندل نمی‌کند. این کار بار و Egress سرور را کاهش داده و برای فایل‌های بزرگ و کلاینت‌های موبایل بهتر است. اعتبارسنجی سخت‌تر است؛ چون آپلود خارج از Resolver شما انجام می‌شود و ممکن است به یک Callback یا مرحله جداگانه برای تأیید آبجکت نیاز داشته باشید.

ملاحظهآپلود سمت سرورPresigned URL
تناسب اندازه فایلکوچک تا متوسط (کمتر از ۱۰۰ مگابایت)فایل‌های بزرگ (۱۰۰ مگابایت به بالا)
هزینه Egress سروربلهخیر
پیچیدگی کلاینتپایینمتوسط
اعتبارسنجی فایلآسان (سمت سرور)سخت‌تر (نیازمند Callback)
پیاده‌سازی در این آموزشبلهخیر

برای تولید یک Presigned URL با AWS SDK Go v2، از یک کلاینت Presign استفاده کنید:

import (
    "context"
    "fmt"
    "os"
    "time"

    "github.com/aws/aws-sdk-go-v2/aws"
    "github.com/aws/aws-sdk-go-v2/service/s3"
)

// GeneratePresignedURL returns a time-limited upload URL for the given object key.
// s3Client is an *s3.Client already configured with your Spaces endpoint and credentials.
func GeneratePresignedURL(s3Client *s3.Client, objectKey string) (string, error) {
    presignClient := s3.NewPresignClient(s3Client)
    presignResult, err := presignClient.PresignPutObject(context.TODO(),
        &s3.PutObjectInput{
            Bucket: aws.String(os.Getenv("DO_SPACE_NAME")),
            Key:    aws.String(objectKey),
        },
        s3.WithPresignExpires(15*time.Minute),
    )
    if err != nil {
        return "", fmt.Errorf("failed to generate presigned URL: %w", err)
    }
    return presignResult.URL, nil
}

توجه: مقدار s3Client اینجا همان ‎*s3.Client پیکربندی‌شده در مرحله ۶ با Endpoint و اطلاعات احراز هویت Spaces شماست. مقدار userFileName را به‌عنوان آرگومان objectKey پاس بدهید. URL برگردانده‌شده ۱۵ دقیقه اعتبار دارد. آن را از Resolver خود برگردانده و اجازه دهید کلاینت فایل را مستقیماً PUT کند.

مدیریت خطا و حالت‌های شکست رایج

جدول زیر به تشخیص شکست‌های رایج هنگام آپلود به Spaces از یک Resolver گراف‌کیوال کمک می‌کند:

خطاعلتراه‌حل
NoCredentialProvidersنبودن یا نادرست بودن ACCESS_KEY / SECRET_KEY در .envتأیید کنید مقادیر .env با اطلاعات احراز هویت API Key مربوط به Spaces مطابقت داشته باشند
InvalidEndpointURL مربوط به Endpoint بدون https:// یا شامل نام Bucketقالب باید https://.digitaloceanspaces.com باشد
RequestError: send request failedرشته Region اشتباهRegion را با Slug مربوط به Region فضای خود مطابقت دهید (مثلاً nyc3)
Scalar با نام Upload در gqlgen برابر nil استکلاینت Content-Type: application/json به‌جای multipart/form-data فرستادهمطمئن شوید کلاینت Content-Type درست را تنظیم کرده و از Specification مربوط به Multipart پیروی می‌کند
panic: not implementedResolver بعد از gqlgen generate به‌طور کامل پیاده‌سازی نشدهpanic(fmt.Errorf(“not implemented”)) را با منطق واقعی Resolver جایگزین کنید
EntityTooLargeفایل از سقفِ هر آبجکت Spaces (‏۵ ترابایت برای آبجکت منفرد) بیشتر استبرای فایل‌های بالای ۵ گیگابایت، از آپلود Multipart با s3.CreateMultipartUpload استفاده کنید

Spaces پارمین کلود از آبجکت‌های منفرد تا ۵ ترابایت پشتیبانی می‌کند. آپلود Multipart برای آبجکت‌های بزرگ‌تر از ۵ گیگابایت الزامی است. برای محدودیت‌ها و سهمیه‌های (Quota) فعلی نرخ، مستندات API مربوط به Spaces را ببینید.

سوالات متداول

۱. آیا GraphQL به‌صورت بومی از آپلود فایل پشتیبانی می‌کند؟

خیر. Specification گراف‌کیوال مکانیزم آپلود فایل تعریف نمی‌کند. آپلود فایل‌ها از طریق graphql-multipart-request-spec هندل می‌شود؛ که GraphQL را روی HTTP با استفاده از رمزنگاری multipart/form-data گسترش می‌دهد. کتابخانه‌هایی مثل gqlgen این Specification را از طریق نوع Scalar با نام Upload پیاده‌سازی می‌کنند.

۲. کدام نسخه از AWS SDK برای Go را باید با Spaces پارمین کلود استفاده کنم؟

از AWS SDK Go v2 ‏(‏github.com/aws/aws-sdk-go-v2) استفاده کنید. AWS SDK Go v1 در حالت نگهداری است و فقط پچ‌های امنیتی بحرانی دریافت می‌کند. Spaces با هر دو سازگار است؛ اما v2 انتخاب توصیه‌شده برای پروژه‌های جدید است.

۳. چطور Endpoint درست را برای Spaces پارمین کلود در AWS SDK Go v2 تنظیم کنم؟

از قالب https://.digitaloceanspaces.com بدون نام Bucket استفاده کنید. مثلاً، برای Region با نام nyc3: ‏https://nyc3.digitaloceanspaces.com. این را از طریق config.WithBaseEndpoint در پیکربندی SDK — همان‌طور که در Resolver مربوط به UploadProfileImage در مرحله ۶ نشان داده شد — تنظیم کنید.

۴. حداکثر اندازه فایل پشتیبانی‌شده توسط یک آپلود منفرد به Spaces پارمین کلود چقدر است؟

یک آپلود آبجکت منفرد از فایل‌هایی تا ۵ ترابایت پشتیبانی می‌کند. آبجکت‌های بزرگ‌تر از ۵ گیگابایت باید از API آپلود Multipart مربوط به S3 استفاده کنند. برای مقادیر فعلی، به مستندات محدودیت‌های Spaces مراجعه کنید.

۵. آیا فایل‌ها را مستقیماً از سرور GraphQL آپلود کنم یا از Presigned URL استفاده کنم؟

برای فایل‌های کوچک و سناریوهایی که به اعتبارسنجی سمت سرور نیاز دارند، آپلود مستقیم از طریق Resolver (همان‌طور که در این آموزش نشان داده شد) رویکرد ساده‌تری است. برای فایل‌های بزرگ یا خطوط لوله با توان عبوری بالا، یک Presigned URL تولید کرده و اجازه دهید کلاینت مستقیماً به Spaces آپلود کند؛ بدون عبور از سرور Go. برای مقایسه کامل، بخش «انتخاب بین آپلود سمت سرور و Presigned URLها» را ببینید.

۶. چطور یک Mutation آپلود فایل GraphQL را بدون Frontend تست کنم؟

از cURL با فلگ -F برای ساخت یک درخواست فرم Multipart که از graphql-multipart-request-spec پیروی می‌کند استفاده کنید. دستور تست در مرحله ۶ ارائه شده است. همچنین ابزارهایی مثل Insomnia از Mutationهای Multipart گراف‌کیوال از طریق یک فیلد آپلود فایل اختصاصی پشتیبانی می‌کنند.

۷. آیا می‌توانم از این رویکرد با سایر ارائه‌دهندگان ذخیره‌سازی Object سازگار با S3 استفاده کنم؟

بله. هر ارائه‌دهنده سازگار با S3 می‌تواند با تغییر URL مربوط به Endpoint در متغیرهای محیطی خود و به‌روزرسانی اطلاعات احراز هویت دسترسی‌تان جایگزین شود. AWS SDK Go v2 وقتی یک Endpoint سفارشی تعیین شده، نسبت به ارائه‌دهنده زیربنایی بی‌طرف است.

جمع‌بندی

در این آموزش، یک آپلود فایل به یک Bucket ساخته‌شده روی یک Space پارمین کلود را با استفاده از AWS SDK برای Go v2 از یک Mutation Resolver در یک اپلیکیشن GraphQL انجام دادید.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا