mirror of
https://github.com/syumai/workers.git
synced 2025-03-10 17:29:11 +00:00
Merge pull request #18 from syumai/add-d1-support
add Cloudflare D1 support
This commit is contained in:
commit
dd17cf216c
@ -23,7 +23,7 @@
|
||||
* [ ] Cache API
|
||||
* [ ] Durable Objects
|
||||
- [x] Calling stubs
|
||||
* [ ] D1
|
||||
* [x] D1 (alpha)
|
||||
* [x] Environment variables
|
||||
|
||||
## Installation
|
||||
|
42
cloudflare/d1/conn.go
Normal file
42
cloudflare/d1/conn.go
Normal file
@ -0,0 +1,42 @@
|
||||
package d1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
dbObj js.Value
|
||||
}
|
||||
|
||||
var (
|
||||
_ driver.Conn = (*Conn)(nil)
|
||||
_ driver.ConnBeginTx = (*Conn)(nil)
|
||||
_ driver.ConnPrepareContext = (*Conn)(nil)
|
||||
)
|
||||
|
||||
func (c *Conn) Prepare(query string) (driver.Stmt, error) {
|
||||
stmtObj := c.dbObj.Call("prepare", query)
|
||||
return &stmt{
|
||||
stmtObj: stmtObj,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Conn) PrepareContext(_ context.Context, query string) (driver.Stmt, error) {
|
||||
return c.Prepare(query)
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
// do nothing
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) Begin() (driver.Tx, error) {
|
||||
return nil, errors.New("d1: Begin is deprecated and not implemented")
|
||||
}
|
||||
|
||||
func (c *Conn) BeginTx(context.Context, driver.TxOptions) (driver.Tx, error) {
|
||||
return nil, errors.New("d1: transaction is not currently supported")
|
||||
}
|
37
cloudflare/d1/connector.go
Normal file
37
cloudflare/d1/connector.go
Normal file
@ -0,0 +1,37 @@
|
||||
package d1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"syscall/js"
|
||||
|
||||
"github.com/syumai/workers/cloudflare/internal/cfruntimecontext"
|
||||
)
|
||||
|
||||
type Connector struct {
|
||||
dbObj js.Value
|
||||
}
|
||||
|
||||
var (
|
||||
_ driver.Connector = (*Connector)(nil)
|
||||
)
|
||||
|
||||
// OpenConnector returns Connector of D1.
|
||||
// This method checks DB existence. If DB was not found, this function returns error.
|
||||
func OpenConnector(ctx context.Context, name string) (driver.Connector, error) {
|
||||
v := cfruntimecontext.GetRuntimeContextEnv(ctx).Get(name)
|
||||
if v.IsUndefined() {
|
||||
return nil, ErrDatabaseNotFound
|
||||
}
|
||||
return &Connector{dbObj: v}, nil
|
||||
}
|
||||
|
||||
// Connect returns Conn of D1.
|
||||
// This method doesn't check DB existence, so this function never return errors.
|
||||
func (c *Connector) Connect(context.Context) (driver.Conn, error) {
|
||||
return &Conn{dbObj: c.dbObj}, nil
|
||||
}
|
||||
|
||||
func (c *Connector) Driver() driver.Driver {
|
||||
return &Driver{}
|
||||
}
|
21
cloudflare/d1/driver.go
Normal file
21
cloudflare/d1/driver.go
Normal file
@ -0,0 +1,21 @@
|
||||
package d1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
sql.Register("d1", &Driver{})
|
||||
}
|
||||
|
||||
type Driver struct{}
|
||||
|
||||
var (
|
||||
_ driver.Driver = (*Driver)(nil)
|
||||
)
|
||||
|
||||
func (d *Driver) Open(string) (driver.Conn, error) {
|
||||
return nil, errors.New("d1: Open is not supported. use d1.OpenConnector and sql.OpenDB instead")
|
||||
}
|
9
cloudflare/d1/errors.go
Normal file
9
cloudflare/d1/errors.go
Normal file
@ -0,0 +1,9 @@
|
||||
package d1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatabaseNotFound = errors.New("d1: database not found")
|
||||
)
|
30
cloudflare/d1/result.go
Normal file
30
cloudflare/d1/result.go
Normal file
@ -0,0 +1,30 @@
|
||||
package d1
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
type result struct {
|
||||
resultObj js.Value
|
||||
}
|
||||
|
||||
var (
|
||||
_ sql.Result = (*result)(nil)
|
||||
)
|
||||
|
||||
// LastInsertId returns id of result's last row.
|
||||
// If lastRowId can't be retrieved, this method returns error.
|
||||
func (r *result) LastInsertId() (int64, error) {
|
||||
v := r.resultObj.Get("meta").Get("lastRowId")
|
||||
if v.IsNull() || v.IsUndefined() {
|
||||
return 0, errors.New("d1: lastRowId cannot be retrieved")
|
||||
}
|
||||
id := v.Int()
|
||||
return int64(id), nil
|
||||
}
|
||||
|
||||
func (r *result) RowsAffected() (int64, error) {
|
||||
return int64(r.resultObj.Get("changes").Int()), nil
|
||||
}
|
97
cloudflare/d1/rows.go
Normal file
97
cloudflare/d1/rows.go
Normal file
@ -0,0 +1,97 @@
|
||||
package d1
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"syscall/js"
|
||||
|
||||
"github.com/syumai/workers/internal/jsutil"
|
||||
)
|
||||
|
||||
type rows struct {
|
||||
rowsObj js.Value
|
||||
currentRow int
|
||||
// columns is cached value of Columns method.
|
||||
// do not use this directly.
|
||||
_columns []string
|
||||
onceColumns sync.Once
|
||||
// _rowsLen is cached value of rowsLen method.
|
||||
// do not use this directly.
|
||||
_rowsLen int
|
||||
onceRowsLen sync.Once
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var _ driver.Rows = (*rows)(nil)
|
||||
|
||||
// Columns returns column names retrieved from query result object's keys.
|
||||
// If rows are empty, this returns nil.
|
||||
func (r *rows) Columns() []string {
|
||||
r.onceColumns.Do(func() {
|
||||
if r.rowsObj.Length() == 0 {
|
||||
// return nothing when row count is zero.
|
||||
return
|
||||
}
|
||||
colsArray := jsutil.ObjectClass.Call("keys", r.rowsObj.Index(0))
|
||||
colsLen := colsArray.Length()
|
||||
cols := make([]string, colsLen)
|
||||
for i := 0; i < colsLen; i++ {
|
||||
cols[i] = colsArray.Index(i).String()
|
||||
}
|
||||
r._columns = cols
|
||||
})
|
||||
return r._columns
|
||||
}
|
||||
|
||||
func (r *rows) Close() error {
|
||||
// do nothing
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertRowColumnValueToDriverValue converts row column's value in JS to Go's driver.Value.
|
||||
// row column value is `null | Number | String | ArrayBuffer`.
|
||||
// see: https://developers.cloudflare.com/d1/platform/client-api/#type-conversion
|
||||
func convertRowColumnValueToAny(v js.Value) (driver.Value, error) {
|
||||
switch v.Type() {
|
||||
case js.TypeNull:
|
||||
return nil, nil
|
||||
case js.TypeNumber:
|
||||
// TODO: handle INTEGER type.
|
||||
return v.Float(), nil
|
||||
case js.TypeString:
|
||||
return v.String(), nil
|
||||
case js.TypeObject:
|
||||
// TODO: handle BLOB type (ArrayBuffer).
|
||||
// see: https://developers.cloudflare.com/d1/platform/client-api/#type-conversion
|
||||
return nil, errors.New("d1: row column value type object is not currently supported")
|
||||
}
|
||||
return nil, errors.New("d1: unexpected row column value type")
|
||||
}
|
||||
|
||||
func (r *rows) Next(dest []driver.Value) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.currentRow == r.rowsLen() {
|
||||
return io.EOF
|
||||
}
|
||||
rowObj := r.rowsObj.Index(r.currentRow)
|
||||
cols := r.Columns()
|
||||
for i, col := range cols {
|
||||
v, err := convertRowColumnValueToAny(rowObj.Get(col))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dest[i] = v
|
||||
}
|
||||
r.currentRow++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rows) rowsLen() int {
|
||||
r.onceRowsLen.Do(func() {
|
||||
r._rowsLen = r.rowsObj.Length()
|
||||
})
|
||||
return r._rowsLen
|
||||
}
|
73
cloudflare/d1/stmt.go
Normal file
73
cloudflare/d1/stmt.go
Normal file
@ -0,0 +1,73 @@
|
||||
package d1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"syscall/js"
|
||||
|
||||
"github.com/syumai/workers/internal/jsutil"
|
||||
)
|
||||
|
||||
type stmt struct {
|
||||
stmtObj js.Value
|
||||
}
|
||||
|
||||
var (
|
||||
_ driver.Stmt = (*stmt)(nil)
|
||||
_ driver.StmtExecContext = (*stmt)(nil)
|
||||
_ driver.StmtQueryContext = (*stmt)(nil)
|
||||
)
|
||||
|
||||
func (s *stmt) Close() error {
|
||||
// do nothing
|
||||
return nil
|
||||
}
|
||||
|
||||
// NumInput is not supported and always returns -1.
|
||||
func (s *stmt) NumInput() int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (s *stmt) Exec([]driver.Value) (driver.Result, error) {
|
||||
return nil, errors.New("d1: Exec is deprecated and not implemented")
|
||||
}
|
||||
|
||||
// ExecContext executes prepared statement.
|
||||
// Given []drier.NamedValue's `Name` field will be ignored because Cloudflare D1 client doesn't support it.
|
||||
func (s *stmt) ExecContext(_ context.Context, args []driver.NamedValue) (driver.Result, error) {
|
||||
argValues := make([]any, len(args))
|
||||
for i, arg := range args {
|
||||
argValues[i] = arg.Value
|
||||
}
|
||||
resultPromise := s.stmtObj.Call("bind", argValues...).Call("run")
|
||||
resultObj, err := jsutil.AwaitPromise(resultPromise)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result{
|
||||
resultObj: resultObj,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *stmt) Query([]driver.Value) (driver.Rows, error) {
|
||||
return nil, errors.New("d1: Query is deprecated and not implemented")
|
||||
}
|
||||
|
||||
func (s *stmt) QueryContext(_ context.Context, args []driver.NamedValue) (driver.Rows, error) {
|
||||
argValues := make([]any, len(args))
|
||||
for i, arg := range args {
|
||||
argValues[i] = arg
|
||||
}
|
||||
resultPromise := s.stmtObj.Call("bind", argValues...).Call("all")
|
||||
rowsObj, err := jsutil.AwaitPromise(resultPromise)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !rowsObj.Get("success").Bool() {
|
||||
return nil, errors.New("d1: failed to query")
|
||||
}
|
||||
return &rows{
|
||||
rowsObj: rowsObj.Get("results"),
|
||||
}, nil
|
||||
}
|
@ -2,11 +2,13 @@ package cloudflare
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/syumai/workers/cloudflare/internal/cfruntimecontext"
|
||||
)
|
||||
|
||||
// Getenv gets a value of an environment variable.
|
||||
// - https://developers.cloudflare.com/workers/platform/environment-variables/
|
||||
// - This function panics when a runtime context is not found.
|
||||
func Getenv(ctx context.Context, name string) string {
|
||||
return getRuntimeContextEnv(ctx).Get(name).String()
|
||||
return cfruntimecontext.GetRuntimeContextEnv(ctx).Get(name).String()
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package cloudflare
|
||||
package cfruntimecontext
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -19,17 +19,17 @@ import (
|
||||
* - see: https://github.com/cloudflare/workers-types/blob/c8d9533caa4415c2156d2cf1daca75289d01ae70/index.d.ts#LL564
|
||||
*/
|
||||
|
||||
// getRuntimeContextEnv gets object which holds environment variables bound to Cloudflare worker.
|
||||
// GetRuntimeContextEnv gets object which holds environment variables bound to Cloudflare worker.
|
||||
// - see: https://github.com/cloudflare/workers-types/blob/c8d9533caa4415c2156d2cf1daca75289d01ae70/index.d.ts#L566
|
||||
func getRuntimeContextEnv(ctx context.Context) js.Value {
|
||||
func GetRuntimeContextEnv(ctx context.Context) js.Value {
|
||||
runtimeCtxValue := runtimecontext.MustExtract(ctx)
|
||||
return runtimeCtxValue.Get("env")
|
||||
}
|
||||
|
||||
// getExecutionContext gets ExecutionContext object from context.
|
||||
// GetExecutionContext gets ExecutionContext object from context.
|
||||
// - see: https://github.com/cloudflare/workers-types/blob/c8d9533caa4415c2156d2cf1daca75289d01ae70/index.d.ts#L567
|
||||
// - see also: https://github.com/cloudflare/workers-types/blob/c8d9533caa4415c2156d2cf1daca75289d01ae70/index.d.ts#L554
|
||||
func getExecutionContext(ctx context.Context) js.Value {
|
||||
func GetExecutionContext(ctx context.Context) js.Value {
|
||||
runtimeCtxValue := runtimecontext.MustExtract(ctx)
|
||||
return runtimeCtxValue.Get("ctx")
|
||||
}
|
@ -6,6 +6,8 @@ import (
|
||||
"io"
|
||||
"syscall/js"
|
||||
|
||||
"github.com/syumai/workers/cloudflare/internal/cfruntimecontext"
|
||||
|
||||
"github.com/syumai/workers/internal/jsutil"
|
||||
)
|
||||
|
||||
@ -21,7 +23,7 @@ type KVNamespace struct {
|
||||
// - if the given variable name doesn't exist on runtime context, returns error.
|
||||
// - This function panics when a runtime context is not found.
|
||||
func NewKVNamespace(ctx context.Context, varName string) (*KVNamespace, error) {
|
||||
inst := getRuntimeContextEnv(ctx).Get(varName)
|
||||
inst := cfruntimecontext.GetRuntimeContextEnv(ctx).Get(varName)
|
||||
if inst.IsUndefined() {
|
||||
return nil, fmt.Errorf("%s is undefined", varName)
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"syscall/js"
|
||||
|
||||
"github.com/syumai/workers/cloudflare/internal/cfruntimecontext"
|
||||
"github.com/syumai/workers/internal/jsutil"
|
||||
)
|
||||
|
||||
@ -22,7 +23,7 @@ type R2Bucket struct {
|
||||
// - if the given variable name doesn't exist on runtime context, returns error.
|
||||
// - This function panics when a runtime context is not found.
|
||||
func NewR2Bucket(ctx context.Context, varName string) (*R2Bucket, error) {
|
||||
inst := getRuntimeContextEnv(ctx).Get(varName)
|
||||
inst := cfruntimecontext.GetRuntimeContextEnv(ctx).Get(varName)
|
||||
if inst.IsUndefined() {
|
||||
return nil, fmt.Errorf("%s is undefined", varName)
|
||||
}
|
||||
|
12
examples/d1-blog-server/Makefile
Normal file
12
examples/d1-blog-server/Makefile
Normal file
@ -0,0 +1,12 @@
|
||||
.PHONY: dev
|
||||
dev:
|
||||
wrangler dev
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
mkdir -p dist
|
||||
tinygo build -o ./dist/app.wasm -target wasm ./main.go
|
||||
|
||||
.PHONY: publish
|
||||
publish:
|
||||
wrangler publish
|
82
examples/d1-blog-server/README.md
Normal file
82
examples/d1-blog-server/README.md
Normal file
@ -0,0 +1,82 @@
|
||||
# d1-blog-server
|
||||
|
||||
* A simple Blog server implemented in Go and compiled with tinygo.
|
||||
* This example is using Cloudflare D1.
|
||||
|
||||
# WIP
|
||||
|
||||
## Example
|
||||
|
||||
* https://d1-blog-server.syumai.workers.dev
|
||||
|
||||
### Create blog post
|
||||
|
||||
```
|
||||
$ curl --location --request POST 'https://d1-blog-server.syumai.workers.dev/articles' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"title":"example post",
|
||||
"body":"body of the example post"
|
||||
}'
|
||||
{
|
||||
"article": {
|
||||
{
|
||||
"id": "f9e8119e-881e-4dc5-9307-af4f2dc79891",
|
||||
"title": "example post",
|
||||
"body": "body of the example post",
|
||||
"createdAt": 1677382874
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### List blog posts
|
||||
|
||||
```
|
||||
$ curl 'https://d1-blog-server.syumai.workers.dev/articles'
|
||||
{
|
||||
"articles": [
|
||||
{
|
||||
"id": "bea6cd80-5a83-45f0-b061-0e13a2ad5fba",
|
||||
"title": "example post 2",
|
||||
"body": "body of the example post 2",
|
||||
"createdAt": 1677383758
|
||||
},
|
||||
{
|
||||
"id": "f9e8119e-881e-4dc5-9307-af4f2dc79891",
|
||||
"title": "example post",
|
||||
"body": "body of the example post",
|
||||
"createdAt": 1677382874
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Requirements
|
||||
|
||||
This project requires these tools to be installed globally.
|
||||
|
||||
* wrangler
|
||||
* tinygo
|
||||
* [easyjson](https://github.com/mailru/easyjson)
|
||||
- `go install github.com/mailru/easyjson/...@latest`
|
||||
|
||||
### Commands
|
||||
|
||||
* Before development, 1. create your own D1 database, 2. set database ID to wrangler.toml and run `wrangler d1 migrations apply [DB Name]`.
|
||||
|
||||
```
|
||||
make dev # run dev server
|
||||
make build # build Go Wasm binary
|
||||
make publish # publish worker
|
||||
```
|
||||
|
||||
## Author
|
||||
|
||||
syumai
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
128
examples/d1-blog-server/app/handler.go
Normal file
128
examples/d1-blog-server/app/handler.go
Normal file
@ -0,0 +1,128 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mailru/easyjson"
|
||||
"github.com/syumai/workers/cloudflare/d1"
|
||||
_ "github.com/syumai/workers/cloudflare/d1" // register driver
|
||||
"github.com/syumai/workers/examples/d1-blog-server/app/model"
|
||||
)
|
||||
|
||||
type articleHandler struct{}
|
||||
|
||||
var _ http.Handler = (*articleHandler)(nil)
|
||||
|
||||
func NewArticleHandler() http.Handler {
|
||||
return &articleHandler{}
|
||||
}
|
||||
|
||||
func (h *articleHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// initialize DB.
|
||||
// D1 connector requires request's context to initialize DB.
|
||||
c, err := d1.OpenConnector(req.Context(), "BlogDB")
|
||||
if err != nil {
|
||||
h.handleErr(w, http.StatusInternalServerError, fmt.Sprintf("failed to initialize DB: %v", err))
|
||||
}
|
||||
// use sql.OpenDB instead of sql.Open.
|
||||
db := sql.OpenDB(c)
|
||||
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
h.listArticles(w, req, db)
|
||||
return
|
||||
case http.MethodPost:
|
||||
h.createArticle(w, req, db)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write([]byte("not found"))
|
||||
}
|
||||
|
||||
func (h *articleHandler) handleErr(w http.ResponseWriter, status int, msg string) {
|
||||
w.WriteHeader(status)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
func (h *articleHandler) createArticle(w http.ResponseWriter, req *http.Request, db *sql.DB) {
|
||||
var createArticleReq model.CreateArticleRequest
|
||||
if err := easyjson.UnmarshalFromReader(req.Body, &createArticleReq); err != nil {
|
||||
h.handleErr(w, http.StatusBadRequest,
|
||||
"request format is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
article := model.Article{
|
||||
ID: uuid.New().String(),
|
||||
Title: createArticleReq.Title,
|
||||
Body: createArticleReq.Body,
|
||||
CreatedAt: uint64(now),
|
||||
}
|
||||
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO articles (id, title, body, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, article.ID, article.Title, article.Body, article.CreatedAt)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
h.handleErr(w, http.StatusInternalServerError,
|
||||
"failed to save article")
|
||||
return
|
||||
}
|
||||
|
||||
res := model.CreateArticleResponse{
|
||||
Article: article,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if _, err := easyjson.MarshalToWriter(&res, w); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to encode response: %w\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *articleHandler) listArticles(w http.ResponseWriter, req *http.Request, db *sql.DB) {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, title, body, created_at FROM articles
|
||||
ORDER BY created_at DESC;
|
||||
`)
|
||||
if err != nil {
|
||||
h.handleErr(w, http.StatusInternalServerError,
|
||||
"failed to load article")
|
||||
return
|
||||
}
|
||||
|
||||
articles := []model.Article{}
|
||||
for rows.Next() {
|
||||
var (
|
||||
id, title, body string
|
||||
createdAt float64 // number value is always retrieved as float64.
|
||||
)
|
||||
err = rows.Scan(&id, &title, &body, &createdAt)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
articles = append(articles, model.Article{
|
||||
ID: id,
|
||||
Title: title,
|
||||
Body: body,
|
||||
CreatedAt: uint64(createdAt),
|
||||
})
|
||||
}
|
||||
res := model.ListArticlesResponse{
|
||||
Articles: articles,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if _, err := easyjson.MarshalToWriter(&res, w); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to encode response: %w\n", err)
|
||||
}
|
||||
}
|
26
examples/d1-blog-server/app/model/article.go
Normal file
26
examples/d1-blog-server/app/model/article.go
Normal file
@ -0,0 +1,26 @@
|
||||
//go:generate easyjson .
|
||||
package model
|
||||
|
||||
//easyjson:json
|
||||
type Article struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
CreatedAt uint64 `json:"createdAt"`
|
||||
}
|
||||
|
||||
//easyjson:json
|
||||
type CreateArticleRequest struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
//easyjson:json
|
||||
type CreateArticleResponse struct {
|
||||
Article Article `json:"article"`
|
||||
}
|
||||
|
||||
//easyjson:json
|
||||
type ListArticlesResponse struct {
|
||||
Articles []Article `json:"articles"`
|
||||
}
|
343
examples/d1-blog-server/app/model/model_easyjson.go
Normal file
343
examples/d1-blog-server/app/model/model_easyjson.go
Normal file
@ -0,0 +1,343 @@
|
||||
// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
json "encoding/json"
|
||||
easyjson "github.com/mailru/easyjson"
|
||||
jlexer "github.com/mailru/easyjson/jlexer"
|
||||
jwriter "github.com/mailru/easyjson/jwriter"
|
||||
)
|
||||
|
||||
// suppress unused package warning
|
||||
var (
|
||||
_ *json.RawMessage
|
||||
_ *jlexer.Lexer
|
||||
_ *jwriter.Writer
|
||||
_ easyjson.Marshaler
|
||||
)
|
||||
|
||||
func easyjsonC80ae7adDecodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel(in *jlexer.Lexer, out *ListArticlesResponse) {
|
||||
isTopLevel := in.IsStart()
|
||||
if in.IsNull() {
|
||||
if isTopLevel {
|
||||
in.Consumed()
|
||||
}
|
||||
in.Skip()
|
||||
return
|
||||
}
|
||||
in.Delim('{')
|
||||
for !in.IsDelim('}') {
|
||||
key := in.UnsafeFieldName(false)
|
||||
in.WantColon()
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
in.WantComma()
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "articles":
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
out.Articles = nil
|
||||
} else {
|
||||
in.Delim('[')
|
||||
if out.Articles == nil {
|
||||
if !in.IsDelim(']') {
|
||||
out.Articles = make([]Article, 0, 1)
|
||||
} else {
|
||||
out.Articles = []Article{}
|
||||
}
|
||||
} else {
|
||||
out.Articles = (out.Articles)[:0]
|
||||
}
|
||||
for !in.IsDelim(']') {
|
||||
var v1 Article
|
||||
(v1).UnmarshalEasyJSON(in)
|
||||
out.Articles = append(out.Articles, v1)
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim(']')
|
||||
}
|
||||
default:
|
||||
in.SkipRecursive()
|
||||
}
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim('}')
|
||||
if isTopLevel {
|
||||
in.Consumed()
|
||||
}
|
||||
}
|
||||
func easyjsonC80ae7adEncodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel(out *jwriter.Writer, in ListArticlesResponse) {
|
||||
out.RawByte('{')
|
||||
first := true
|
||||
_ = first
|
||||
{
|
||||
const prefix string = ",\"articles\":"
|
||||
out.RawString(prefix[1:])
|
||||
if in.Articles == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
|
||||
out.RawString("null")
|
||||
} else {
|
||||
out.RawByte('[')
|
||||
for v2, v3 := range in.Articles {
|
||||
if v2 > 0 {
|
||||
out.RawByte(',')
|
||||
}
|
||||
(v3).MarshalEasyJSON(out)
|
||||
}
|
||||
out.RawByte(']')
|
||||
}
|
||||
}
|
||||
out.RawByte('}')
|
||||
}
|
||||
|
||||
// MarshalJSON supports json.Marshaler interface
|
||||
func (v ListArticlesResponse) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{}
|
||||
easyjsonC80ae7adEncodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel(&w, v)
|
||||
return w.Buffer.BuildBytes(), w.Error
|
||||
}
|
||||
|
||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||
func (v ListArticlesResponse) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
easyjsonC80ae7adEncodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel(w, v)
|
||||
}
|
||||
|
||||
// UnmarshalJSON supports json.Unmarshaler interface
|
||||
func (v *ListArticlesResponse) UnmarshalJSON(data []byte) error {
|
||||
r := jlexer.Lexer{Data: data}
|
||||
easyjsonC80ae7adDecodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel(&r, v)
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||
func (v *ListArticlesResponse) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||
easyjsonC80ae7adDecodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel(l, v)
|
||||
}
|
||||
func easyjsonC80ae7adDecodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel1(in *jlexer.Lexer, out *CreateArticleResponse) {
|
||||
isTopLevel := in.IsStart()
|
||||
if in.IsNull() {
|
||||
if isTopLevel {
|
||||
in.Consumed()
|
||||
}
|
||||
in.Skip()
|
||||
return
|
||||
}
|
||||
in.Delim('{')
|
||||
for !in.IsDelim('}') {
|
||||
key := in.UnsafeFieldName(false)
|
||||
in.WantColon()
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
in.WantComma()
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "article":
|
||||
(out.Article).UnmarshalEasyJSON(in)
|
||||
default:
|
||||
in.SkipRecursive()
|
||||
}
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim('}')
|
||||
if isTopLevel {
|
||||
in.Consumed()
|
||||
}
|
||||
}
|
||||
func easyjsonC80ae7adEncodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel1(out *jwriter.Writer, in CreateArticleResponse) {
|
||||
out.RawByte('{')
|
||||
first := true
|
||||
_ = first
|
||||
{
|
||||
const prefix string = ",\"article\":"
|
||||
out.RawString(prefix[1:])
|
||||
(in.Article).MarshalEasyJSON(out)
|
||||
}
|
||||
out.RawByte('}')
|
||||
}
|
||||
|
||||
// MarshalJSON supports json.Marshaler interface
|
||||
func (v CreateArticleResponse) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{}
|
||||
easyjsonC80ae7adEncodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel1(&w, v)
|
||||
return w.Buffer.BuildBytes(), w.Error
|
||||
}
|
||||
|
||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||
func (v CreateArticleResponse) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
easyjsonC80ae7adEncodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel1(w, v)
|
||||
}
|
||||
|
||||
// UnmarshalJSON supports json.Unmarshaler interface
|
||||
func (v *CreateArticleResponse) UnmarshalJSON(data []byte) error {
|
||||
r := jlexer.Lexer{Data: data}
|
||||
easyjsonC80ae7adDecodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel1(&r, v)
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||
func (v *CreateArticleResponse) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||
easyjsonC80ae7adDecodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel1(l, v)
|
||||
}
|
||||
func easyjsonC80ae7adDecodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel2(in *jlexer.Lexer, out *CreateArticleRequest) {
|
||||
isTopLevel := in.IsStart()
|
||||
if in.IsNull() {
|
||||
if isTopLevel {
|
||||
in.Consumed()
|
||||
}
|
||||
in.Skip()
|
||||
return
|
||||
}
|
||||
in.Delim('{')
|
||||
for !in.IsDelim('}') {
|
||||
key := in.UnsafeFieldName(false)
|
||||
in.WantColon()
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
in.WantComma()
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "title":
|
||||
out.Title = string(in.String())
|
||||
case "body":
|
||||
out.Body = string(in.String())
|
||||
default:
|
||||
in.SkipRecursive()
|
||||
}
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim('}')
|
||||
if isTopLevel {
|
||||
in.Consumed()
|
||||
}
|
||||
}
|
||||
func easyjsonC80ae7adEncodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel2(out *jwriter.Writer, in CreateArticleRequest) {
|
||||
out.RawByte('{')
|
||||
first := true
|
||||
_ = first
|
||||
{
|
||||
const prefix string = ",\"title\":"
|
||||
out.RawString(prefix[1:])
|
||||
out.String(string(in.Title))
|
||||
}
|
||||
{
|
||||
const prefix string = ",\"body\":"
|
||||
out.RawString(prefix)
|
||||
out.String(string(in.Body))
|
||||
}
|
||||
out.RawByte('}')
|
||||
}
|
||||
|
||||
// MarshalJSON supports json.Marshaler interface
|
||||
func (v CreateArticleRequest) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{}
|
||||
easyjsonC80ae7adEncodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel2(&w, v)
|
||||
return w.Buffer.BuildBytes(), w.Error
|
||||
}
|
||||
|
||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||
func (v CreateArticleRequest) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
easyjsonC80ae7adEncodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel2(w, v)
|
||||
}
|
||||
|
||||
// UnmarshalJSON supports json.Unmarshaler interface
|
||||
func (v *CreateArticleRequest) UnmarshalJSON(data []byte) error {
|
||||
r := jlexer.Lexer{Data: data}
|
||||
easyjsonC80ae7adDecodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel2(&r, v)
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||
func (v *CreateArticleRequest) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||
easyjsonC80ae7adDecodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel2(l, v)
|
||||
}
|
||||
func easyjsonC80ae7adDecodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel3(in *jlexer.Lexer, out *Article) {
|
||||
isTopLevel := in.IsStart()
|
||||
if in.IsNull() {
|
||||
if isTopLevel {
|
||||
in.Consumed()
|
||||
}
|
||||
in.Skip()
|
||||
return
|
||||
}
|
||||
in.Delim('{')
|
||||
for !in.IsDelim('}') {
|
||||
key := in.UnsafeFieldName(false)
|
||||
in.WantColon()
|
||||
if in.IsNull() {
|
||||
in.Skip()
|
||||
in.WantComma()
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "id":
|
||||
out.ID = string(in.String())
|
||||
case "title":
|
||||
out.Title = string(in.String())
|
||||
case "body":
|
||||
out.Body = string(in.String())
|
||||
case "createdAt":
|
||||
out.CreatedAt = uint64(in.Uint64())
|
||||
default:
|
||||
in.SkipRecursive()
|
||||
}
|
||||
in.WantComma()
|
||||
}
|
||||
in.Delim('}')
|
||||
if isTopLevel {
|
||||
in.Consumed()
|
||||
}
|
||||
}
|
||||
func easyjsonC80ae7adEncodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel3(out *jwriter.Writer, in Article) {
|
||||
out.RawByte('{')
|
||||
first := true
|
||||
_ = first
|
||||
{
|
||||
const prefix string = ",\"id\":"
|
||||
out.RawString(prefix[1:])
|
||||
out.String(string(in.ID))
|
||||
}
|
||||
{
|
||||
const prefix string = ",\"title\":"
|
||||
out.RawString(prefix)
|
||||
out.String(string(in.Title))
|
||||
}
|
||||
{
|
||||
const prefix string = ",\"body\":"
|
||||
out.RawString(prefix)
|
||||
out.String(string(in.Body))
|
||||
}
|
||||
{
|
||||
const prefix string = ",\"createdAt\":"
|
||||
out.RawString(prefix)
|
||||
out.Uint64(uint64(in.CreatedAt))
|
||||
}
|
||||
out.RawByte('}')
|
||||
}
|
||||
|
||||
// MarshalJSON supports json.Marshaler interface
|
||||
func (v Article) MarshalJSON() ([]byte, error) {
|
||||
w := jwriter.Writer{}
|
||||
easyjsonC80ae7adEncodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel3(&w, v)
|
||||
return w.Buffer.BuildBytes(), w.Error
|
||||
}
|
||||
|
||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||
func (v Article) MarshalEasyJSON(w *jwriter.Writer) {
|
||||
easyjsonC80ae7adEncodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel3(w, v)
|
||||
}
|
||||
|
||||
// UnmarshalJSON supports json.Unmarshaler interface
|
||||
func (v *Article) UnmarshalJSON(data []byte) error {
|
||||
r := jlexer.Lexer{Data: data}
|
||||
easyjsonC80ae7adDecodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel3(&r, v)
|
||||
return r.Error()
|
||||
}
|
||||
|
||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||
func (v *Article) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||
easyjsonC80ae7adDecodeGithubComSyumaiWorkersExamplesD1BlogServerAppModel3(l, v)
|
||||
}
|
15
examples/d1-blog-server/go.mod
Normal file
15
examples/d1-blog-server/go.mod
Normal file
@ -0,0 +1,15 @@
|
||||
module github.com/syumai/workers/examples/d1-blog-server
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/mailru/easyjson v0.7.7
|
||||
github.com/syumai/workers v0.9.0
|
||||
)
|
||||
|
||||
replace github.com/syumai/workers => ../../
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
)
|
6
examples/d1-blog-server/go.sum
Normal file
6
examples/d1-blog-server/go.sum
Normal file
@ -0,0 +1,6 @@
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
13
examples/d1-blog-server/main.go
Normal file
13
examples/d1-blog-server/main.go
Normal file
@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/syumai/workers"
|
||||
"github.com/syumai/workers/examples/d1-blog-server/app"
|
||||
)
|
||||
|
||||
func main() {
|
||||
http.Handle("/articles", app.NewArticleHandler())
|
||||
workers.Serve(nil) // use http.DefaultServeMux
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
-- Migration number: 0000 2023-01-09T14:48:53.705Z
|
||||
CREATE TABLE articles (
|
||||
id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX idx_articles_on_created_at ON articles (created_at DESC);
|
22
examples/d1-blog-server/worker.mjs
Normal file
22
examples/d1-blog-server/worker.mjs
Normal file
@ -0,0 +1,22 @@
|
||||
import "../assets/polyfill_performance.js";
|
||||
import "../assets/wasm_exec.js";
|
||||
import mod from "./dist/app.wasm";
|
||||
|
||||
const go = new Go();
|
||||
|
||||
const readyPromise = new Promise((resolve) => {
|
||||
globalThis.ready = resolve;
|
||||
});
|
||||
|
||||
const load = WebAssembly.instantiate(mod, go.importObject).then((instance) => {
|
||||
go.run(instance);
|
||||
return instance;
|
||||
});
|
||||
|
||||
export default {
|
||||
async fetch(req, env, ctx) {
|
||||
await load;
|
||||
await readyPromise;
|
||||
return handleRequest(req, { env, ctx });
|
||||
}
|
||||
}
|
20
examples/d1-blog-server/wrangler.toml
Normal file
20
examples/d1-blog-server/wrangler.toml
Normal file
@ -0,0 +1,20 @@
|
||||
name = "d1-blog-server"
|
||||
main = "./worker.mjs"
|
||||
compatibility_date = "2023-01-09"
|
||||
|
||||
[build]
|
||||
command = "make build"
|
||||
|
||||
[[ d1_databases ]]
|
||||
binding = "BlogDB"
|
||||
database_name = "d1-blog-server"
|
||||
database_id = "c6d5e4c0-3134-42fa-834c-812b24fea3cf"
|
||||
preview_database_id = "9f44099e-a008-45c2-88e4-aed0bd754a86"
|
||||
|
||||
[[ d1_databases ]]
|
||||
# workaround to migrate preview DB
|
||||
# - https://github.com/cloudflare/wrangler2/issues/2446
|
||||
binding = "BlogDB-Preview"
|
||||
database_name = "d1-blog-server-preview"
|
||||
database_id = "9f44099e-a008-45c2-88e4-aed0bd754a86"
|
||||
preview_database_id = "9f44099e-a008-45c2-88e4-aed0bd754a86"
|
Loading…
x
Reference in New Issue
Block a user