diff --git a/README.md b/README.md index d4b6f88..f254e5d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ * [ ] Cache API * [ ] Durable Objects - [x] Calling stubs -* [ ] D1 +* [x] D1 (alpha) * [x] Environment variables ## Installation diff --git a/cloudflare/d1/conn.go b/cloudflare/d1/conn.go new file mode 100644 index 0000000..ccc8f11 --- /dev/null +++ b/cloudflare/d1/conn.go @@ -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") +} diff --git a/cloudflare/d1/connector.go b/cloudflare/d1/connector.go new file mode 100644 index 0000000..eb10df5 --- /dev/null +++ b/cloudflare/d1/connector.go @@ -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{} +} diff --git a/cloudflare/d1/driver.go b/cloudflare/d1/driver.go new file mode 100644 index 0000000..c67e7af --- /dev/null +++ b/cloudflare/d1/driver.go @@ -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") +} diff --git a/cloudflare/d1/errors.go b/cloudflare/d1/errors.go new file mode 100644 index 0000000..9076fc2 --- /dev/null +++ b/cloudflare/d1/errors.go @@ -0,0 +1,9 @@ +package d1 + +import ( + "errors" +) + +var ( + ErrDatabaseNotFound = errors.New("d1: database not found") +) diff --git a/cloudflare/d1/result.go b/cloudflare/d1/result.go new file mode 100644 index 0000000..5c1edfb --- /dev/null +++ b/cloudflare/d1/result.go @@ -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 +} diff --git a/cloudflare/d1/rows.go b/cloudflare/d1/rows.go new file mode 100644 index 0000000..66dc4d1 --- /dev/null +++ b/cloudflare/d1/rows.go @@ -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 +} diff --git a/cloudflare/d1/stmt.go b/cloudflare/d1/stmt.go new file mode 100644 index 0000000..418e20f --- /dev/null +++ b/cloudflare/d1/stmt.go @@ -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 +} diff --git a/cloudflare/env.go b/cloudflare/env.go index 75c09e6..85a28dd 100644 --- a/cloudflare/env.go +++ b/cloudflare/env.go @@ -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() } diff --git a/cloudflare/runtimecontext.go b/cloudflare/internal/cfruntimecontext/cfruntimecontext.go similarity index 79% rename from cloudflare/runtimecontext.go rename to cloudflare/internal/cfruntimecontext/cfruntimecontext.go index bc3fd2d..cbc2228 100644 --- a/cloudflare/runtimecontext.go +++ b/cloudflare/internal/cfruntimecontext/cfruntimecontext.go @@ -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") } diff --git a/cloudflare/kv.go b/cloudflare/kv.go index 13da052..fd1e935 100644 --- a/cloudflare/kv.go +++ b/cloudflare/kv.go @@ -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) } diff --git a/cloudflare/r2bucket.go b/cloudflare/r2bucket.go index 3697d04..e1d1add 100644 --- a/cloudflare/r2bucket.go +++ b/cloudflare/r2bucket.go @@ -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) } diff --git a/examples/d1-blog-server/Makefile b/examples/d1-blog-server/Makefile new file mode 100644 index 0000000..c9d7430 --- /dev/null +++ b/examples/d1-blog-server/Makefile @@ -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 diff --git a/examples/d1-blog-server/README.md b/examples/d1-blog-server/README.md new file mode 100644 index 0000000..1797e58 --- /dev/null +++ b/examples/d1-blog-server/README.md @@ -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 diff --git a/examples/d1-blog-server/app/handler.go b/examples/d1-blog-server/app/handler.go new file mode 100644 index 0000000..34cde3b --- /dev/null +++ b/examples/d1-blog-server/app/handler.go @@ -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) + } +} diff --git a/examples/d1-blog-server/app/model/article.go b/examples/d1-blog-server/app/model/article.go new file mode 100644 index 0000000..30d538c --- /dev/null +++ b/examples/d1-blog-server/app/model/article.go @@ -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"` +} diff --git a/examples/d1-blog-server/app/model/model_easyjson.go b/examples/d1-blog-server/app/model/model_easyjson.go new file mode 100644 index 0000000..f023fc3 --- /dev/null +++ b/examples/d1-blog-server/app/model/model_easyjson.go @@ -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) +} diff --git a/examples/d1-blog-server/go.mod b/examples/d1-blog-server/go.mod new file mode 100644 index 0000000..d3c34c8 --- /dev/null +++ b/examples/d1-blog-server/go.mod @@ -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 +) diff --git a/examples/d1-blog-server/go.sum b/examples/d1-blog-server/go.sum new file mode 100644 index 0000000..3e8aec9 --- /dev/null +++ b/examples/d1-blog-server/go.sum @@ -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= diff --git a/examples/d1-blog-server/main.go b/examples/d1-blog-server/main.go new file mode 100644 index 0000000..8ae26e0 --- /dev/null +++ b/examples/d1-blog-server/main.go @@ -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 +} diff --git a/examples/d1-blog-server/migrations/0000_create_articles_table.sql b/examples/d1-blog-server/migrations/0000_create_articles_table.sql new file mode 100644 index 0000000..5b09a2f --- /dev/null +++ b/examples/d1-blog-server/migrations/0000_create_articles_table.sql @@ -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); diff --git a/examples/d1-blog-server/worker.mjs b/examples/d1-blog-server/worker.mjs new file mode 100644 index 0000000..649ccf0 --- /dev/null +++ b/examples/d1-blog-server/worker.mjs @@ -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 }); + } +} diff --git a/examples/d1-blog-server/wrangler.toml b/examples/d1-blog-server/wrangler.toml new file mode 100644 index 0000000..2f230dd --- /dev/null +++ b/examples/d1-blog-server/wrangler.toml @@ -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"