Merge pull request #18 from syumai/add-d1-support

add Cloudflare D1 support
This commit is contained in:
syumai 2023-02-26 13:09:03 +09:00 committed by GitHub
commit dd17cf216c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 998 additions and 9 deletions

View File

@ -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
View 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")
}

View 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
View 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
View File

@ -0,0 +1,9 @@
package d1
import (
"errors"
)
var (
ErrDatabaseNotFound = errors.New("d1: database not found")
)

30
cloudflare/d1/result.go Normal file
View 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
View 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
View 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
}

View File

@ -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()
}

View File

@ -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")
}

View File

@ -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)
}

View File

@ -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)
}

View 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

View 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

View 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)
}
}

View 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"`
}

View 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)
}

View 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
)

View 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=

View 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
}

View File

@ -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);

View 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 });
}
}

View 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"