crud.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. package crud
  2. import (
  3. "context"
  4. "git.nspix.com/golang/micro/gateway/http"
  5. "git.nspix.com/golang/rest/orm/query"
  6. "git.nspix.com/golang/rest/orm/schema"
  7. "github.com/jinzhu/inflection"
  8. "github.com/russross/blackfriday"
  9. "gorm.io/gorm"
  10. "sort"
  11. "strings"
  12. "sync"
  13. )
  14. var (
  15. Default = New()
  16. enable int32 = 1
  17. )
  18. type (
  19. EachQueryCallback func(val interface{}, field *schema.Schema, search *query.Query, ctx *http.Context) (err error)
  20. QueryCallback func(val interface{}, query *query.Query, ctx *http.Context) (err error)
  21. SaveCallback func(val interface{}, ctx *http.Context) (err error)
  22. DeleteCallback func(val interface{}, ctx *http.Context) (err error)
  23. Naming interface {
  24. DisplayName() string
  25. }
  26. CRUD struct {
  27. db *gorm.DB
  28. formatter *Formatter
  29. once sync.Once
  30. ctx context.Context
  31. entityLocker sync.RWMutex
  32. entities map[string]*Entity
  33. formatLocker sync.RWMutex
  34. httpSvr *http.Server
  35. formatCallbacks map[string]EachQueryCallback
  36. }
  37. )
  38. func (crud *CRUD) getCallbackByFormat(format string) EachQueryCallback {
  39. crud.formatLocker.RLock()
  40. v, ok := crud.formatCallbacks[format]
  41. crud.formatLocker.RUnlock()
  42. if ok {
  43. return v
  44. }
  45. return nil
  46. }
  47. func (crud *CRUD) Register(module string, value interface{}, opts ...Option) {
  48. o := &Options{ctx: crud.ctx, DB: crud.db}
  49. for _, f := range opts {
  50. f(o)
  51. }
  52. if crud.httpSvr == nil {
  53. return
  54. }
  55. e := newEntity(crud.ctx, value, o.DB)
  56. e.instance = crud
  57. e.Callbacks = o.Callbacks
  58. e.SetModule(module)
  59. if o.enable != nil {
  60. e.enable = o.enable
  61. } else {
  62. e.enable = &enable
  63. }
  64. if o.Formatter == nil {
  65. e.formatter = crud.formatter
  66. } else {
  67. e.formatter = o.Formatter
  68. }
  69. crud.entityLocker.Lock()
  70. crud.entities[e.stmt.Table+"@"+e.Module] = e
  71. crud.entityLocker.Unlock()
  72. e.naming.plural = inflection.Plural(e.stmt.Table)
  73. e.naming.singular = inflection.Singular(e.stmt.Table)
  74. if o.Scenarios == nil || len(o.Scenarios) == 0 {
  75. e.Scenarios = []string{ScenarioList, ScenarioView, ScenarioCreate, ScenarioUpdate, ScenarioDelete, ScenarioExport}
  76. } else {
  77. e.Scenarios = o.Scenarios
  78. }
  79. if v, ok := value.(Naming); ok {
  80. e.naming.label = v.DisplayName()
  81. } else {
  82. e.naming.label = strings.Title(e.naming.singular)
  83. }
  84. e.prefix = o.Prefix
  85. if e.hasScenarios(ScenarioList) {
  86. e.Urls[ScenarioList] = o.Prefix + "/" + e.naming.plural
  87. crud.httpSvr.Handle("GET", e.Urls[ScenarioList], e.actionIndex, o.Middleware...)
  88. }
  89. if e.hasScenarios(ScenarioView) {
  90. e.Urls[ScenarioView] = o.Prefix + "/" + e.naming.singular + "/:id"
  91. crud.httpSvr.Handle("GET", e.Urls[ScenarioView], e.actionView, o.Middleware...)
  92. }
  93. if e.hasScenarios(ScenarioCreate) {
  94. e.Urls[ScenarioCreate] = o.Prefix + "/" + e.naming.singular
  95. crud.httpSvr.Handle("POST", e.Urls[ScenarioCreate], e.actionCreate, o.Middleware...)
  96. }
  97. if e.hasScenarios(ScenarioUpdate) {
  98. e.Urls[ScenarioUpdate] = o.Prefix + "/" + e.naming.singular + "/:id"
  99. crud.httpSvr.Handle("PUT", e.Urls[ScenarioUpdate], e.actionUpdate, o.Middleware...)
  100. }
  101. if e.hasScenarios(ScenarioDelete) {
  102. e.Urls[ScenarioDelete] = o.Prefix + "/" + e.naming.singular + "/:id"
  103. crud.httpSvr.Handle("DELETE", e.Urls[ScenarioDelete], e.actionDelete, o.Middleware...)
  104. }
  105. if e.hasScenarios(ScenarioExport) {
  106. e.Urls[ScenarioExport] = o.Prefix + "/" + e.naming.singular + "-export"
  107. crud.httpSvr.Handle("GET", e.Urls[ScenarioExport], e.actionExport, o.Middleware...)
  108. }
  109. }
  110. func (crud *CRUD) AttachFormatQueryHook(format string, cb EachQueryCallback) {
  111. crud.formatLocker.Lock()
  112. crud.formatCallbacks[format] = cb
  113. crud.formatLocker.Unlock()
  114. }
  115. func (crud *CRUD) SetDB(db *gorm.DB) {
  116. crud.db = db
  117. }
  118. func (crud *CRUD) GetDB() *gorm.DB {
  119. return crud.db
  120. }
  121. func (crud *CRUD) SetHttpServer(svr *http.Server) {
  122. crud.httpSvr = svr
  123. crud.once.Do(func() {
  124. crud.bindDocAction()
  125. })
  126. }
  127. func (crud *CRUD) SetContext(ctx context.Context) {
  128. crud.ctx = ctx
  129. if crud.httpSvr == nil {
  130. crud.httpSvr = http.FromContext(ctx)
  131. }
  132. if crud.db == nil {
  133. crud.db = ctx.Value("db").(*gorm.DB)
  134. }
  135. crud.once.Do(func() {
  136. crud.bindDocAction()
  137. })
  138. }
  139. func (crud *CRUD) readSnapshot() Entities {
  140. crud.entityLocker.RLock()
  141. i := 0
  142. es := make(Entities, len(crud.entities))
  143. for _, e := range crud.entities {
  144. es[i] = e
  145. i++
  146. }
  147. crud.entityLocker.RUnlock()
  148. sort.Sort(es)
  149. return es
  150. }
  151. func (crud *CRUD) actionCatalog(c *http.Context) (err error) {
  152. var sb strings.Builder
  153. sb.WriteString("# 接口目录")
  154. sb.WriteByte('\n')
  155. sb.WriteByte('\n')
  156. es := crud.readSnapshot()
  157. for _, e := range es {
  158. sb.WriteString("* [" + e.naming.label + "接口](/crud/doc/" + e.stmt.Table + "@" + e.Module + ")")
  159. sb.WriteByte('\n')
  160. }
  161. c.Response().Header().Set("Content-Type", "text/html; charset=utf-8")
  162. _, _ = c.Response().Write([]byte(`<html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel="stylesheet" href="//rd-api.nspix.com/assets/docs/doc.css"></head><body><div class="markdown-body"><aside id="markdown-toc"></aside><article id="markdown-body">`))
  163. _, _ = c.Response().Write(blackfriday.Run([]byte(sb.String())))
  164. _, _ = c.Response().Write([]byte(`</article></div><script src="//rd-api.nspix.com/assets/docs/doc.js"></script></body></html>`))
  165. return
  166. }
  167. func (crud *CRUD) actionDocs(c *http.Context) (err error) {
  168. crud.entityLocker.RLock()
  169. entity, ok := crud.entities[c.ParamValue("id")]
  170. crud.entityLocker.RUnlock()
  171. if !ok {
  172. return c.Error(4004, "record not found")
  173. }
  174. c.Response().Header().Set("Content-Type", "text/html; charset=utf-8")
  175. _, _ = c.Response().Write([]byte(`<html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel="stylesheet" href="//rd-api.nspix.com/assets/docs/doc.css"><title>` + entity.naming.label + `</title></head><body><div class="markdown-body"><aside id="markdown-toc"></aside><article id="markdown-body">`))
  176. _, _ = c.Response().Write(blackfriday.Run(crud.generateDoc(entity)))
  177. _, _ = c.Response().Write([]byte(`</article></div><script src="//rd-api.nspix.com/assets/docs/doc.js"></script></body></html>`))
  178. return
  179. }
  180. func (crud *CRUD) actionEntities(c *http.Context) (err error) {
  181. es := crud.readSnapshot()
  182. names := make([]map[string]interface{}, 0)
  183. for _, e := range es {
  184. names = append(names, map[string]interface{}{
  185. "module": e.Module,
  186. "table": e.stmt.Table,
  187. "label": e.naming.label,
  188. "url": e.prefix + "/" + e.naming.singular,
  189. "urls": e.Urls,
  190. "scenarios": e.Scenarios,
  191. })
  192. }
  193. return c.Success(names)
  194. }
  195. func (crud *CRUD) bindDocAction() {
  196. if crud.httpSvr != nil {
  197. crud.httpSvr.Handle("GET", "/crud/entities", crud.actionEntities)
  198. crud.httpSvr.Handle("GET", "/crud/docs", crud.actionCatalog)
  199. crud.httpSvr.Handle("GET", "/crud/doc/:id", crud.actionDocs)
  200. }
  201. }
  202. func New() *CRUD {
  203. return &CRUD{
  204. formatter: DefaultFormat,
  205. ctx: context.Background(),
  206. formatCallbacks: make(map[string]EachQueryCallback),
  207. entities: make(map[string]*Entity),
  208. }
  209. }