@x47base/pocketbase-addon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.dockerignore +9 -0
  2. package/Dockerfile +26 -0
  3. package/FEATURES.md +32 -0
  4. package/LICENSE.md +17 -0
  5. package/MIGRATION.md +49 -0
  6. package/NOTICE.md +5 -0
  7. package/README.md +26 -0
  8. package/adapter.go +31 -0
  9. package/admin/register.go +60 -0
  10. package/admin/register_test.go +37 -0
  11. package/backups/backup_encryption_test.go +82 -0
  12. package/backups/encryption.go +138 -0
  13. package/backups/integration_test.go +51 -0
  14. package/backups/register.go +120 -0
  15. package/backups/restore.go +119 -0
  16. package/backups/s3_test.go +52 -0
  17. package/backups/swap.go +53 -0
  18. package/backups/swap_test.go +75 -0
  19. package/backups/upload.go +52 -0
  20. package/bin/pocketbase-extension.mjs +25 -0
  21. package/cmd/edge/main.go +123 -0
  22. package/cmd/import-fork/main.go +37 -0
  23. package/cmd/loadtest/main.go +61 -0
  24. package/cmd/loadtest/sandbox.go +73 -0
  25. package/cmd/loadtest/sandbox_test.go +20 -0
  26. package/cmd/pocketbase/main.go +78 -0
  27. package/deploy/README.md +148 -0
  28. package/deploy/app/hooks/README.md +2 -0
  29. package/deploy/app/migrations/1789000000_notes.js +16 -0
  30. package/deploy/app/public/README.md +2 -0
  31. package/deploy/compose.secrets.yaml +8 -0
  32. package/deploy/compose.yaml +68 -0
  33. package/deploy/edge.json +13 -0
  34. package/edge/gateway.go +295 -0
  35. package/edge/gateway_test.go +296 -0
  36. package/edge/openapi.json +1 -0
  37. package/edge/policy.go +150 -0
  38. package/features/collection_singleton.go +13 -0
  39. package/features/collection_singleton_test.go +46 -0
  40. package/features/dimensions_test.go +65 -0
  41. package/features/duplicate.go +178 -0
  42. package/features/duplicate_test.go +128 -0
  43. package/features/field_color.go +46 -0
  44. package/features/field_date_only.go +39 -0
  45. package/features/field_json_schema.go +92 -0
  46. package/features/field_scalar_extensions_test.go +66 -0
  47. package/features/files.go +36 -0
  48. package/features/filter_has_any_test.go +81 -0
  49. package/features/generate_test.go +72 -0
  50. package/features/has_any_visibility_test.go +62 -0
  51. package/features/json.go +50 -0
  52. package/features/membership.go +64 -0
  53. package/features/register.go +45 -0
  54. package/features/schema_test.go +58 -0
  55. package/features/ui/main.js +133 -0
  56. package/features/ui/settings.js +31 -0
  57. package/go.mod +54 -0
  58. package/go.sum +159 -0
  59. package/internal/archive/create.go +91 -0
  60. package/internal/archive/create_test.go +125 -0
  61. package/internal/archive/extract.go +99 -0
  62. package/internal/archive/extract_test.go +88 -0
  63. package/jsvm/binds.go +1273 -0
  64. package/jsvm/binds_app_reset_test.go +314 -0
  65. package/jsvm/binds_test.go +1870 -0
  66. package/jsvm/form_data.go +149 -0
  67. package/jsvm/form_data_test.go +225 -0
  68. package/jsvm/internal/types/generated/embed.go +6 -0
  69. package/jsvm/internal/types/generated/types.d.ts +24820 -0
  70. package/jsvm/internal/types/types.go +1408 -0
  71. package/jsvm/jsvm.go +587 -0
  72. package/jsvm/mapper.go +67 -0
  73. package/jsvm/mapper_test.go +42 -0
  74. package/jsvm/pool.go +73 -0
  75. package/jsvm/program_source_test.go +24 -0
  76. package/loadtest/loadtest.go +202 -0
  77. package/loadtest/loadtest_test.go +84 -0
  78. package/localization/README.md +23 -0
  79. package/localization/catalogue.json +483 -0
  80. package/localization/localization.go +94 -0
  81. package/localization/localization_test.go +21 -0
  82. package/mail/register.go +80 -0
  83. package/mail/register_test.go +49 -0
  84. package/mail/resolve.go +91 -0
  85. package/migration/import.go +96 -0
  86. package/migration/import_test.go +58 -0
  87. package/otp/otp.go +56 -0
  88. package/otp/otp_test.go +56 -0
  89. package/package.json +51 -0
  90. package/scripts/check-edge.py +42 -0
  91. package/scripts/check.sh +11 -0
  92. package/scripts/sync-jsvm-types.sh +10 -0
  93. package/security/README.md +94 -0
  94. package/security/assurance_test.go +149 -0
  95. package/security/compatibility_test.go +128 -0
  96. package/security/config.go +78 -0
  97. package/security/dashboard_test.go +103 -0
  98. package/security/management.go +169 -0
  99. package/security/openapi.json +508 -0
  100. package/security/review.go +34 -0
  101. package/security/security.go +503 -0
  102. package/security/security_test.go +146 -0
  103. package/security/state.go +116 -0
  104. package/security/ui/dashboard.css +4 -0
  105. package/security/ui/dashboard.js +83 -0
  106. package/security/ui/main.js +15 -0
  107. package/security/ui/model.js +32 -0
  108. package/security/ui/model.test.mjs +25 -0
  109. package/security/ui/registration.test.mjs +10 -0
  110. package/settings/env_test.go +41 -0
  111. package/settings/openapi.json +193 -0
  112. package/settings/settings.go +155 -0
  113. package/settings/settings_test.go +31 -0
  114. package/watcher/watcher.go +192 -0
  115. package/watcher/watcher_test.go +200 -0
  116. package/web/static.go +99 -0
  117. package/web/static_test.go +48 -0
package/jsvm/binds.go ADDED
@@ -0,0 +1,1273 @@
1
+ package jsvm
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json/v2"
7
+ "errors"
8
+ "io"
9
+ "io/fs"
10
+ "log/slog"
11
+ "net/http"
12
+ "os"
13
+ "os/exec"
14
+ "path/filepath"
15
+ "reflect"
16
+ "slices"
17
+ "sort"
18
+ "strings"
19
+ "time"
20
+
21
+ "github.com/dop251/goja"
22
+ "github.com/golang-jwt/jwt/v5"
23
+ "github.com/pocketbase/dbx"
24
+ validation "github.com/pocketbase/ozzo-validation/v4"
25
+ "github.com/pocketbase/pocketbase/apis"
26
+ "github.com/pocketbase/pocketbase/core"
27
+ "github.com/pocketbase/pocketbase/forms"
28
+ "github.com/pocketbase/pocketbase/mails"
29
+ "github.com/pocketbase/pocketbase/tools/filesystem"
30
+ "github.com/pocketbase/pocketbase/tools/hook"
31
+ "github.com/pocketbase/pocketbase/tools/inflector"
32
+ "github.com/pocketbase/pocketbase/tools/mailer"
33
+ "github.com/pocketbase/pocketbase/tools/router"
34
+ "github.com/pocketbase/pocketbase/tools/security"
35
+ "github.com/pocketbase/pocketbase/tools/store"
36
+ "github.com/pocketbase/pocketbase/tools/subscriptions"
37
+ "github.com/pocketbase/pocketbase/tools/types"
38
+ "github.com/spf13/cast"
39
+ "github.com/spf13/cobra"
40
+ )
41
+
42
+ // hooksBinds adds wrapped "on*" hook methods by reflecting on core.App.
43
+ func hooksBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
44
+ fm := FieldMapper{}
45
+
46
+ appType := reflect.TypeOf(app)
47
+ appValue := reflect.ValueOf(app)
48
+ totalMethods := appType.NumMethod()
49
+ excludeHooks := []string{"OnServe"}
50
+
51
+ for i := 0; i < totalMethods; i++ {
52
+ method := appType.Method(i)
53
+ if !strings.HasPrefix(method.Name, "On") || slices.Contains(excludeHooks, method.Name) {
54
+ continue // not a hook or excluded
55
+ }
56
+
57
+ jsName := fm.MethodName(appType, method)
58
+
59
+ // register the hook to the loader
60
+ loader.Set(jsName, func(callback string, tags ...string) {
61
+ // overwrite the global $app with the hook scoped instance
62
+ callback = `function(e) { $app = e.app; return (` + callback + `).call(undefined, e) }`
63
+ pr := goja.MustCompile(defaultScriptPath, "{("+callback+").apply(undefined, __args)}", true)
64
+
65
+ tagsAsValues := make([]reflect.Value, len(tags))
66
+ for i, tag := range tags {
67
+ tagsAsValues[i] = reflect.ValueOf(tag)
68
+ }
69
+
70
+ hookInstance := appValue.MethodByName(method.Name).Call(tagsAsValues)[0]
71
+ hookBindFunc := hookInstance.MethodByName("BindFunc")
72
+
73
+ handlerType := hookBindFunc.Type().In(0)
74
+
75
+ handler := reflect.MakeFunc(handlerType, func(args []reflect.Value) (results []reflect.Value) {
76
+ handlerArgs := make([]any, len(args))
77
+ for i, arg := range args {
78
+ handlerArgs[i] = arg.Interface()
79
+ }
80
+
81
+ err := executors.run(func(executor *goja.Runtime) error {
82
+ oldApp := executor.Get("$app")
83
+ executor.Set("__args", handlerArgs)
84
+ res, err := executor.RunProgram(pr)
85
+ executor.Set("__args", goja.Undefined())
86
+ executor.Set("$app", oldApp) // reset to its default for the executor
87
+
88
+ // check for returned Go error value
89
+ if resErr := checkGojaValueForError(app, res); resErr != nil {
90
+ return resErr
91
+ }
92
+
93
+ return normalizeException(err)
94
+ })
95
+
96
+ return []reflect.Value{reflect.ValueOf(&err).Elem()}
97
+ })
98
+
99
+ // register the wrapped hook handler
100
+ hookBindFunc.Call([]reflect.Value{handler})
101
+ })
102
+ }
103
+ }
104
+
105
+ func cronBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
106
+ cronAdd := func(jobId, cronExpr, handler string) {
107
+ pr := goja.MustCompile(defaultScriptPath, "{("+handler+").apply(undefined)}", true)
108
+
109
+ err := app.Cron().Add(jobId, cronExpr, func() {
110
+ err := executors.run(func(executor *goja.Runtime) error {
111
+ _, err := executor.RunProgram(pr)
112
+ return err
113
+ })
114
+
115
+ if err != nil {
116
+ app.Logger().Error(
117
+ "[cronAdd] failed to execute cron job",
118
+ slog.String("jobId", jobId),
119
+ slog.String("error", err.Error()),
120
+ )
121
+ }
122
+ })
123
+ if err != nil {
124
+ panic("[cronAdd] failed to register cron job " + jobId + ": " + err.Error())
125
+ }
126
+ }
127
+ loader.Set("cronAdd", cronAdd)
128
+
129
+ cronRemove := func(jobId string) {
130
+ app.Cron().Remove(jobId)
131
+ }
132
+ loader.Set("cronRemove", cronRemove)
133
+
134
+ // register the removal helper also in the executors to allow removing cron jobs from everywhere
135
+ oldFactory := executors.factory
136
+ executors.factory = func() *goja.Runtime {
137
+ vm := oldFactory()
138
+
139
+ vm.Set("cronAdd", cronAdd)
140
+ vm.Set("cronRemove", cronRemove)
141
+
142
+ return vm
143
+ }
144
+ for _, item := range executors.items {
145
+ item.vm.Set("cronAdd", cronAdd)
146
+ item.vm.Set("cronRemove", cronRemove)
147
+ }
148
+ }
149
+
150
+ func routerBinds(app core.App, loader *goja.Runtime, executors *vmsPool) {
151
+ loader.Set("routerAdd", func(method string, path string, handler goja.Value, middlewares ...goja.Value) {
152
+ wrappedMiddlewares, err := wrapMiddlewares(executors, middlewares...)
153
+ if err != nil {
154
+ panic("[routerAdd] failed to wrap middlewares: " + err.Error())
155
+ }
156
+
157
+ wrappedHandler, err := wrapHandlerFunc(executors, handler)
158
+ if err != nil {
159
+ panic("[routerAdd] failed to wrap handler: " + err.Error())
160
+ }
161
+
162
+ app.OnServe().BindFunc(func(e *core.ServeEvent) error {
163
+ e.Router.Route(strings.ToUpper(method), path, wrappedHandler).Bind(wrappedMiddlewares...)
164
+
165
+ return e.Next()
166
+ })
167
+ })
168
+
169
+ loader.Set("routerUse", func(middlewares ...goja.Value) {
170
+ wrappedMiddlewares, err := wrapMiddlewares(executors, middlewares...)
171
+ if err != nil {
172
+ panic("[routerUse] failed to wrap middlewares: " + err.Error())
173
+ }
174
+
175
+ app.OnServe().BindFunc(func(e *core.ServeEvent) error {
176
+ e.Router.Bind(wrappedMiddlewares...)
177
+ return e.Next()
178
+ })
179
+ })
180
+ }
181
+
182
+ func wrapHandlerFunc(executors *vmsPool, handler goja.Value) (func(*core.RequestEvent) error, error) {
183
+ if handler == nil {
184
+ return nil, errors.New("handler must be non-nil")
185
+ }
186
+
187
+ switch h := handler.Export().(type) {
188
+ case func(*core.RequestEvent) error:
189
+ // "native" handler func - no need to wrap
190
+ return h, nil
191
+ case func(goja.FunctionCall) goja.Value, string:
192
+ pr := goja.MustCompile(defaultScriptPath, "{("+handler.String()+").apply(undefined, __args)}", true)
193
+
194
+ wrappedHandler := func(e *core.RequestEvent) error {
195
+ return executors.run(func(executor *goja.Runtime) error {
196
+ oldApp := executor.Get("$app")
197
+ executor.Set("$app", e.App) // overwrite the global $app with the hook scoped instance
198
+ executor.Set("__args", []any{e})
199
+ res, err := executor.RunProgram(pr)
200
+ executor.Set("__args", goja.Undefined())
201
+ executor.Set("$app", oldApp)
202
+
203
+ // check for returned Go error value
204
+ if resErr := checkGojaValueForError(e.App, res); resErr != nil {
205
+ return resErr
206
+ }
207
+
208
+ return normalizeException(err)
209
+ })
210
+ }
211
+
212
+ return wrappedHandler, nil
213
+ default:
214
+ return nil, errors.New("unsupported goja handler type")
215
+ }
216
+ }
217
+
218
+ type gojaHookHandler struct {
219
+ id string
220
+ serializedFunc string
221
+ priority int
222
+ }
223
+
224
+ func wrapMiddlewares(executors *vmsPool, rawMiddlewares ...goja.Value) ([]*hook.Handler[*core.RequestEvent], error) {
225
+ wrappedMiddlewares := make([]*hook.Handler[*core.RequestEvent], len(rawMiddlewares))
226
+
227
+ for i, m := range rawMiddlewares {
228
+ if m == nil {
229
+ return nil, errors.New("middleware must be non-nil")
230
+ }
231
+
232
+ switch v := m.Export().(type) {
233
+ case *hook.Handler[*core.RequestEvent]:
234
+ // "native" middleware handler - no need to wrap
235
+ wrappedMiddlewares[i] = v
236
+ case func(*core.RequestEvent) error:
237
+ // "native" middleware func - wrap as handler
238
+ wrappedMiddlewares[i] = &hook.Handler[*core.RequestEvent]{
239
+ Func: v,
240
+ }
241
+ case *gojaHookHandler:
242
+ if v.serializedFunc == "" {
243
+ return nil, errors.New("missing or invalid Middleware function")
244
+ }
245
+
246
+ pr := goja.MustCompile(defaultScriptPath, "{("+v.serializedFunc+").apply(undefined, __args)}", true)
247
+
248
+ wrappedMiddlewares[i] = &hook.Handler[*core.RequestEvent]{
249
+ Id: v.id,
250
+ Priority: v.priority,
251
+ Func: func(e *core.RequestEvent) error {
252
+ return executors.run(func(executor *goja.Runtime) error {
253
+ oldApp := executor.Get("$app")
254
+ executor.Set("$app", e.App) // overwrite the global $app with the hook scoped instance
255
+ executor.Set("__args", []any{e})
256
+ res, err := executor.RunProgram(pr)
257
+ executor.Set("__args", goja.Undefined())
258
+ executor.Set("$app", oldApp)
259
+
260
+ // check for returned Go error value
261
+ if resErr := checkGojaValueForError(e.App, res); resErr != nil {
262
+ return resErr
263
+ }
264
+
265
+ return normalizeException(err)
266
+ })
267
+ },
268
+ }
269
+ case func(goja.FunctionCall) goja.Value, string:
270
+ pr := goja.MustCompile(defaultScriptPath, "{("+m.String()+").apply(undefined, __args)}", true)
271
+
272
+ wrappedMiddlewares[i] = &hook.Handler[*core.RequestEvent]{
273
+ Func: func(e *core.RequestEvent) error {
274
+ return executors.run(func(executor *goja.Runtime) error {
275
+ oldApp := executor.Get("$app")
276
+ executor.Set("$app", e.App) // overwrite the global $app with the hook scoped instance
277
+ executor.Set("__args", []any{e})
278
+ res, err := executor.RunProgram(pr)
279
+ executor.Set("__args", goja.Undefined())
280
+ executor.Set("$app", oldApp)
281
+
282
+ // check for returned Go error value
283
+ if resErr := checkGojaValueForError(e.App, res); resErr != nil {
284
+ return resErr
285
+ }
286
+
287
+ return normalizeException(err)
288
+ })
289
+ },
290
+ }
291
+ default:
292
+ return nil, errors.New("unsupported goja middleware type")
293
+ }
294
+ }
295
+
296
+ return wrappedMiddlewares, nil
297
+ }
298
+
299
+ // -------------------------------------------------------------------
300
+
301
+ var cachedArrayOfTypes = store.New[reflect.Type, reflect.Type](nil)
302
+
303
+ // BindCore registers common core objects and functions such as sleep,
304
+ // toString, DynamicModel, etc. into the provided runtime.
305
+ func BindCore(vm *goja.Runtime) {
306
+ vm.SetFieldNameMapper(FieldMapper{})
307
+
308
+ // deprecated: use toString
309
+ vm.Set("readerToString", func(r io.Reader, maxBytes int) (string, error) {
310
+ if maxBytes == 0 {
311
+ maxBytes = router.DefaultMaxMemory
312
+ }
313
+
314
+ limitReader := io.LimitReader(r, int64(maxBytes))
315
+
316
+ bodyBytes, readErr := io.ReadAll(limitReader)
317
+ if readErr != nil {
318
+ return "", readErr
319
+ }
320
+
321
+ return string(bodyBytes), nil
322
+ })
323
+
324
+ // note: throw only on reader error
325
+ vm.Set("toBytes", func(raw any, maxReaderBytes int) ([]byte, error) {
326
+ switch v := raw.(type) {
327
+ case nil:
328
+ return []byte{}, nil
329
+ case string:
330
+ return []byte(v), nil
331
+ case []byte:
332
+ return v, nil
333
+ case types.JSONRaw:
334
+ return v, nil
335
+ case io.Reader:
336
+ if maxReaderBytes == 0 {
337
+ maxReaderBytes = router.DefaultMaxMemory
338
+ }
339
+
340
+ limitReader := io.LimitReader(v, int64(maxReaderBytes))
341
+
342
+ return io.ReadAll(limitReader)
343
+ default:
344
+ b, err := cast.ToUint8SliceE(v)
345
+ if err == nil {
346
+ return b, nil
347
+ }
348
+
349
+ str, err := cast.ToStringE(v)
350
+ if err == nil {
351
+ return []byte(str), nil
352
+ }
353
+
354
+ // as a last attempt try to json encode the value
355
+ rawBytes, _ := json.Marshal(raw, json.Deterministic(true))
356
+
357
+ return rawBytes, nil
358
+ }
359
+ })
360
+
361
+ // note: throw only on reader error
362
+ vm.Set("toString", func(raw any, maxReaderBytes int) (string, error) {
363
+ switch v := raw.(type) {
364
+ case io.Reader:
365
+ if maxReaderBytes == 0 {
366
+ maxReaderBytes = router.DefaultMaxMemory
367
+ }
368
+
369
+ limitReader := io.LimitReader(v, int64(maxReaderBytes))
370
+
371
+ bodyBytes, readErr := io.ReadAll(limitReader)
372
+ if readErr != nil {
373
+ return "", readErr
374
+ }
375
+
376
+ return string(bodyBytes), nil
377
+ default:
378
+ str, err := cast.ToStringE(v)
379
+ if err == nil {
380
+ return str, nil
381
+ }
382
+
383
+ // as a last attempt try to json encode the value
384
+ rawBytes, _ := json.Marshal(raw, json.Deterministic(true))
385
+
386
+ return string(rawBytes), nil
387
+ }
388
+ })
389
+
390
+ vm.Set("sleep", func(milliseconds int64) {
391
+ time.Sleep(time.Duration(milliseconds) * time.Millisecond)
392
+ })
393
+
394
+ vm.Set("arrayOf", func(model any) any {
395
+ mt := reflect.TypeOf(model)
396
+ st := cachedArrayOfTypes.GetOrSet(mt, func() reflect.Type {
397
+ return reflect.SliceOf(mt)
398
+ })
399
+
400
+ return reflect.New(st).Elem().Addr().Interface()
401
+ })
402
+
403
+ vm.Set("unmarshal", func(data, dst any) error {
404
+ raw, err := json.Marshal(data)
405
+ if err != nil {
406
+ return err
407
+ }
408
+
409
+ return json.Unmarshal(raw, &dst)
410
+ })
411
+
412
+ vm.Set("Context", func(call goja.ConstructorCall) *goja.Object {
413
+ var instance context.Context
414
+
415
+ oldCtx, ok := call.Argument(0).Export().(context.Context)
416
+ if ok {
417
+ instance = oldCtx
418
+ } else {
419
+ instance = context.Background()
420
+ }
421
+
422
+ key := call.Argument(1).Export()
423
+ if key != nil {
424
+ instance = context.WithValue(instance, key, call.Argument(2).Export())
425
+ }
426
+
427
+ instanceValue := vm.ToValue(instance).(*goja.Object)
428
+ instanceValue.SetPrototype(call.This.Prototype())
429
+
430
+ return instanceValue
431
+ })
432
+
433
+ vm.Set("DynamicModel", func(call goja.ConstructorCall) *goja.Object {
434
+ shape, ok := call.Argument(0).Export().(map[string]any)
435
+ if !ok || len(shape) == 0 {
436
+ panic("[DynamicModel] missing shape data")
437
+ }
438
+
439
+ instance := newDynamicModel(shape)
440
+ instanceValue := vm.ToValue(instance).(*goja.Object)
441
+ instanceValue.SetPrototype(call.This.Prototype())
442
+
443
+ return instanceValue
444
+ })
445
+
446
+ // nullable helpers usually used as DynamicModel shape values
447
+ vm.Set("nullString", func() *string {
448
+ var v string
449
+ return &v
450
+ })
451
+ vm.Set("nullFloat", func() *float64 {
452
+ var v float64
453
+ return &v
454
+ })
455
+ vm.Set("nullInt", func() *int64 {
456
+ var v int64
457
+ return &v
458
+ })
459
+ vm.Set("nullBool", func() *bool {
460
+ var v bool
461
+ return &v
462
+ })
463
+ vm.Set("nullArray", func() *types.JSONArray[any] {
464
+ var v types.JSONArray[any]
465
+ return &v
466
+ })
467
+ vm.Set("nullObject", func() *types.JSONMap[any] {
468
+ var v types.JSONMap[any]
469
+ return &v
470
+ })
471
+
472
+ vm.Set("Record", func(call goja.ConstructorCall) *goja.Object {
473
+ var instance *core.Record
474
+
475
+ collection, ok := call.Argument(0).Export().(*core.Collection)
476
+ if ok {
477
+ instance = core.NewRecord(collection)
478
+ data, ok := call.Argument(1).Export().(map[string]any)
479
+ if ok {
480
+ instance.Load(data)
481
+ }
482
+ } else {
483
+ instance = &core.Record{}
484
+ }
485
+
486
+ instanceValue := vm.ToValue(instance).(*goja.Object)
487
+ instanceValue.SetPrototype(call.This.Prototype())
488
+
489
+ return instanceValue
490
+ })
491
+
492
+ vm.Set("Collection", func(call goja.ConstructorCall) *goja.Object {
493
+ instance := &core.Collection{}
494
+ return structConstructorUnmarshal(vm, call, instance)
495
+ })
496
+
497
+ vm.Set("FieldsList", func(call goja.ConstructorCall) *goja.Object {
498
+ instance := &core.FieldsList{}
499
+ return structConstructorUnmarshal(vm, call, instance)
500
+ })
501
+
502
+ // fields
503
+ // ---
504
+ vm.Set("Field", func(call goja.ConstructorCall) *goja.Object {
505
+ data, _ := call.Argument(0).Export().(map[string]any)
506
+ rawDataSlice, _ := json.Marshal([]any{data})
507
+
508
+ fieldsList := core.NewFieldsList()
509
+ _ = fieldsList.UnmarshalJSON(rawDataSlice)
510
+
511
+ if len(fieldsList) == 0 {
512
+ return nil
513
+ }
514
+
515
+ field := fieldsList[0]
516
+
517
+ fieldValue := vm.ToValue(field).(*goja.Object)
518
+ fieldValue.SetPrototype(call.This.Prototype())
519
+
520
+ return fieldValue
521
+ })
522
+ vm.Set("NumberField", func(call goja.ConstructorCall) *goja.Object {
523
+ instance := &core.NumberField{}
524
+ return structConstructorUnmarshal(vm, call, instance)
525
+ })
526
+ vm.Set("BoolField", func(call goja.ConstructorCall) *goja.Object {
527
+ instance := &core.BoolField{}
528
+ return structConstructorUnmarshal(vm, call, instance)
529
+ })
530
+ vm.Set("TextField", func(call goja.ConstructorCall) *goja.Object {
531
+ instance := &core.TextField{}
532
+ return structConstructorUnmarshal(vm, call, instance)
533
+ })
534
+ vm.Set("URLField", func(call goja.ConstructorCall) *goja.Object {
535
+ instance := &core.URLField{}
536
+ return structConstructorUnmarshal(vm, call, instance)
537
+ })
538
+ vm.Set("EmailField", func(call goja.ConstructorCall) *goja.Object {
539
+ instance := &core.EmailField{}
540
+ return structConstructorUnmarshal(vm, call, instance)
541
+ })
542
+ vm.Set("EditorField", func(call goja.ConstructorCall) *goja.Object {
543
+ instance := &core.EditorField{}
544
+ return structConstructorUnmarshal(vm, call, instance)
545
+ })
546
+ vm.Set("PasswordField", func(call goja.ConstructorCall) *goja.Object {
547
+ instance := &core.PasswordField{}
548
+ return structConstructorUnmarshal(vm, call, instance)
549
+ })
550
+ vm.Set("DateField", func(call goja.ConstructorCall) *goja.Object {
551
+ instance := &core.DateField{}
552
+ return structConstructorUnmarshal(vm, call, instance)
553
+ })
554
+ vm.Set("AutodateField", func(call goja.ConstructorCall) *goja.Object {
555
+ instance := &core.AutodateField{}
556
+ return structConstructorUnmarshal(vm, call, instance)
557
+ })
558
+ vm.Set("JSONField", func(call goja.ConstructorCall) *goja.Object {
559
+ instance := &core.JSONField{}
560
+ return structConstructorUnmarshal(vm, call, instance)
561
+ })
562
+ vm.Set("RelationField", func(call goja.ConstructorCall) *goja.Object {
563
+ instance := &core.RelationField{}
564
+ return structConstructorUnmarshal(vm, call, instance)
565
+ })
566
+ vm.Set("SelectField", func(call goja.ConstructorCall) *goja.Object {
567
+ instance := &core.SelectField{}
568
+ return structConstructorUnmarshal(vm, call, instance)
569
+ })
570
+ vm.Set("FileField", func(call goja.ConstructorCall) *goja.Object {
571
+ instance := &core.FileField{}
572
+ return structConstructorUnmarshal(vm, call, instance)
573
+ })
574
+ vm.Set("GeoPointField", func(call goja.ConstructorCall) *goja.Object {
575
+ instance := &core.GeoPointField{}
576
+ return structConstructorUnmarshal(vm, call, instance)
577
+ })
578
+ // ---
579
+
580
+ vm.Set("MailerMessage", func(call goja.ConstructorCall) *goja.Object {
581
+ instance := &mailer.Message{}
582
+ return structConstructor(vm, call, instance)
583
+ })
584
+
585
+ vm.Set("Command", func(call goja.ConstructorCall) *goja.Object {
586
+ instance := &cobra.Command{}
587
+ return structConstructor(vm, call, instance)
588
+ })
589
+
590
+ vm.Set("RequestInfo", func(call goja.ConstructorCall) *goja.Object {
591
+ instance := &core.RequestInfo{Context: core.RequestInfoContextDefault}
592
+ return structConstructor(vm, call, instance)
593
+ })
594
+
595
+ // ```js
596
+ // new Middleware((e) => {
597
+ // return e.next()
598
+ // }, 100, "example_middleware")
599
+ // ```
600
+ vm.Set("Middleware", func(call goja.ConstructorCall) *goja.Object {
601
+ instance := &gojaHookHandler{}
602
+
603
+ instance.serializedFunc = call.Argument(0).String()
604
+ instance.priority = cast.ToInt(call.Argument(1).Export())
605
+ instance.id = cast.ToString(call.Argument(2).Export())
606
+
607
+ instanceValue := vm.ToValue(instance).(*goja.Object)
608
+ instanceValue.SetPrototype(call.This.Prototype())
609
+
610
+ return instanceValue
611
+ })
612
+
613
+ // note: named Timezone to avoid conflicts with the JS Location interface.
614
+ vm.Set("Timezone", func(call goja.ConstructorCall) *goja.Object {
615
+ name, _ := call.Argument(0).Export().(string)
616
+
617
+ instance, err := time.LoadLocation(name)
618
+ if err != nil {
619
+ instance = time.UTC
620
+ }
621
+
622
+ instanceValue := vm.ToValue(instance).(*goja.Object)
623
+ instanceValue.SetPrototype(call.This.Prototype())
624
+
625
+ return instanceValue
626
+ })
627
+
628
+ vm.Set("DateTime", func(call goja.ConstructorCall) *goja.Object {
629
+ instance := types.NowDateTime()
630
+
631
+ rawDate, _ := call.Argument(0).Export().(string)
632
+ locName, _ := call.Argument(1).Export().(string)
633
+ if rawDate != "" && locName != "" {
634
+ loc, err := time.LoadLocation(locName)
635
+ if err != nil {
636
+ loc = time.UTC
637
+ }
638
+
639
+ instance, _ = types.ParseDateTime(cast.ToTimeInDefaultLocation(rawDate, loc))
640
+ } else if rawDate != "" {
641
+ // forward directly to ParseDateTime to preserve the original behavior
642
+ instance, _ = types.ParseDateTime(rawDate)
643
+ }
644
+
645
+ instanceValue := vm.ToValue(instance).(*goja.Object)
646
+ instanceValue.SetPrototype(call.This.Prototype())
647
+
648
+ return structConstructor(vm, call, instance)
649
+ })
650
+
651
+ vm.Set("ValidationError", func(call goja.ConstructorCall) *goja.Object {
652
+ code, _ := call.Argument(0).Export().(string)
653
+ message, _ := call.Argument(1).Export().(string)
654
+
655
+ instance := validation.NewError(code, message)
656
+ instanceValue := vm.ToValue(instance).(*goja.Object)
657
+ instanceValue.SetPrototype(call.This.Prototype())
658
+
659
+ return instanceValue
660
+ })
661
+
662
+ vm.Set("Cookie", func(call goja.ConstructorCall) *goja.Object {
663
+ instance := &http.Cookie{}
664
+ return structConstructor(vm, call, instance)
665
+ })
666
+
667
+ vm.Set("SubscriptionMessage", func(call goja.ConstructorCall) *goja.Object {
668
+ instance := &subscriptions.Message{}
669
+ return structConstructor(vm, call, instance)
670
+ })
671
+ }
672
+
673
+ // BindDbx registers $dbx.* namespaced object with dbx database builder related methods.
674
+ //
675
+ // See https://pocketbase.io/jsvm/modules/_dbx.html.
676
+ func BindDbx(vm *goja.Runtime) {
677
+ obj := vm.NewObject()
678
+ vm.Set("$dbx", obj)
679
+
680
+ obj.Set("exp", dbx.NewExp)
681
+ obj.Set("hashExp", func(data map[string]any) dbx.HashExp {
682
+ return dbx.HashExp(data)
683
+ })
684
+ obj.Set("not", dbx.Not)
685
+ obj.Set("and", dbx.And)
686
+ obj.Set("or", dbx.Or)
687
+ obj.Set("in", dbx.In)
688
+ obj.Set("notIn", dbx.NotIn)
689
+ obj.Set("like", dbx.Like)
690
+ obj.Set("orLike", dbx.OrLike)
691
+ obj.Set("notLike", dbx.NotLike)
692
+ obj.Set("orNotLike", dbx.OrNotLike)
693
+ obj.Set("exists", dbx.Exists)
694
+ obj.Set("notExists", dbx.NotExists)
695
+ obj.Set("between", dbx.Between)
696
+ obj.Set("notBetween", dbx.NotBetween)
697
+ }
698
+
699
+ // BindMails registers $mail.* namespaced object with common mail related helpers.
700
+ //
701
+ // See https://pocketbase.io/jsvm/modules/_mails.html.
702
+ func BindMails(vm *goja.Runtime) {
703
+ obj := vm.NewObject()
704
+ vm.Set("$mails", obj)
705
+
706
+ obj.Set("sendRecordPasswordReset", mails.SendRecordPasswordReset)
707
+ obj.Set("sendRecordVerification", mails.SendRecordVerification)
708
+ obj.Set("sendRecordChangeEmail", mails.SendRecordChangeEmail)
709
+ obj.Set("sendRecordOTP", mails.SendRecordOTP)
710
+ obj.Set("sendRecordAuthAlert", mails.SendRecordAuthAlert)
711
+ }
712
+
713
+ // BindSecurity registers $security.* namespaced object with common security related helpers.
714
+ //
715
+ // See https://pocketbase.io/jsvm/modules/_security.html.
716
+ func BindSecurity(vm *goja.Runtime) {
717
+ obj := vm.NewObject()
718
+ vm.Set("$security", obj)
719
+
720
+ // crypto
721
+ obj.Set("md5", security.MD5)
722
+ obj.Set("sha256", security.SHA256)
723
+ obj.Set("sha512", security.SHA512)
724
+ obj.Set("hs256", security.HS256)
725
+ obj.Set("hs512", security.HS512)
726
+ obj.Set("equal", security.Equal)
727
+
728
+ // random
729
+ obj.Set("randomString", security.RandomString)
730
+ obj.Set("randomStringByRegex", security.RandomStringByRegex)
731
+ obj.Set("randomStringWithAlphabet", security.RandomStringWithAlphabet)
732
+ obj.Set("pseudorandomString", security.PseudorandomString)
733
+ obj.Set("pseudorandomStringWithAlphabet", security.PseudorandomStringWithAlphabet)
734
+
735
+ // jwt
736
+ obj.Set("parseUnverifiedJWT", func(token string) (map[string]any, error) {
737
+ return security.ParseUnverifiedJWT(token)
738
+ })
739
+ obj.Set("parseJWT", func(token string, verificationKey string) (map[string]any, error) {
740
+ return security.ParseJWT(token, verificationKey)
741
+ })
742
+ obj.Set("createJWT", func(payload jwt.MapClaims, signingKey string, secDuration int) (string, error) {
743
+ return security.NewJWT(payload, signingKey, time.Duration(secDuration)*time.Second)
744
+ })
745
+
746
+ // encryption
747
+ obj.Set("encrypt", security.Encrypt)
748
+ obj.Set("decrypt", func(cipherText, key string) (string, error) {
749
+ result, err := security.Decrypt(cipherText, key)
750
+
751
+ if err != nil {
752
+ return "", err
753
+ }
754
+
755
+ return string(result), err
756
+ })
757
+ }
758
+
759
+ // BindFilesystem registers $filesystem.* namespaced object with
760
+ // common filesystem package related helpers.
761
+ //
762
+ // See https://pocketbase.io/jsvm/modules/_filesystem.html.
763
+ func BindFilesystem(vm *goja.Runtime) {
764
+ obj := vm.NewObject()
765
+ vm.Set("$filesystem", obj)
766
+
767
+ obj.Set("s3", filesystem.NewS3)
768
+ obj.Set("local", filesystem.NewLocal)
769
+ obj.Set("fileFromPath", filesystem.NewFileFromPath)
770
+ obj.Set("fileFromBytes", filesystem.NewFileFromBytes)
771
+ obj.Set("fileFromMultipart", filesystem.NewFileFromMultipart)
772
+ obj.Set("fileFromURL", func(url string, secTimeout int) (*filesystem.File, error) {
773
+ if secTimeout == 0 {
774
+ secTimeout = 120
775
+ }
776
+
777
+ ctx, cancel := context.WithTimeout(context.Background(), time.Duration(secTimeout)*time.Second)
778
+ defer cancel()
779
+
780
+ return filesystem.NewFileFromURL(ctx, url)
781
+ })
782
+ }
783
+
784
+ // BindFilepath registers $filepath.* namespaced object with
785
+ // common std Go filepath package related exports.
786
+ //
787
+ // See https://pocketbase.io/jsvm/modules/_filepath.html.
788
+ func BindFilepath(vm *goja.Runtime) {
789
+ obj := vm.NewObject()
790
+ vm.Set("$filepath", obj)
791
+
792
+ obj.Set("base", filepath.Base)
793
+ obj.Set("clean", filepath.Clean)
794
+ obj.Set("dir", filepath.Dir)
795
+ obj.Set("ext", filepath.Ext)
796
+ obj.Set("fromSlash", filepath.FromSlash)
797
+ obj.Set("glob", filepath.Glob)
798
+ obj.Set("isAbs", filepath.IsAbs)
799
+ obj.Set("join", filepath.Join)
800
+ obj.Set("match", filepath.Match)
801
+ obj.Set("rel", filepath.Rel)
802
+ obj.Set("split", filepath.Split)
803
+ obj.Set("splitList", filepath.SplitList)
804
+ obj.Set("toSlash", filepath.ToSlash)
805
+ obj.Set("walk", filepath.Walk)
806
+ obj.Set("walkDir", filepath.WalkDir)
807
+ }
808
+
809
+ // BindOS registers $os.* namespaced object with
810
+ // common std Go os package related exports.
811
+ //
812
+ // See https://pocketbase.io/jsvm/modules/_os.html.
813
+ func BindOS(vm *goja.Runtime) {
814
+ obj := vm.NewObject()
815
+ vm.Set("$os", obj)
816
+
817
+ obj.Set("args", os.Args)
818
+ obj.Set("exec", exec.Command) // @deprecated
819
+ obj.Set("cmd", exec.Command)
820
+ obj.Set("exit", os.Exit)
821
+ obj.Set("getenv", os.Getenv)
822
+ obj.Set("dirFS", os.DirFS)
823
+ obj.Set("stat", os.Stat)
824
+ obj.Set("readFile", os.ReadFile)
825
+ obj.Set("writeFile", os.WriteFile)
826
+ obj.Set("readDir", os.ReadDir)
827
+ obj.Set("tempDir", os.TempDir)
828
+ obj.Set("truncate", os.Truncate)
829
+ obj.Set("getwd", os.Getwd)
830
+ obj.Set("mkdir", os.Mkdir)
831
+ obj.Set("mkdirAll", os.MkdirAll)
832
+ obj.Set("rename", os.Rename)
833
+ obj.Set("remove", os.Remove)
834
+ obj.Set("removeAll", os.RemoveAll)
835
+ obj.Set("openRoot", os.OpenRoot)
836
+ obj.Set("openInRoot", os.OpenInRoot)
837
+ }
838
+
839
+ // BindForms registers various application form constructors.
840
+ // These bindings are mostly used internally and/or preserved for backward compatibility with earlier versions.
841
+ func BindForms(vm *goja.Runtime) {
842
+ registerFactoryAsConstructor(vm, "AppleClientSecretCreateForm", forms.NewAppleClientSecretCreate)
843
+ registerFactoryAsConstructor(vm, "RecordUpsertForm", forms.NewRecordUpsert)
844
+ registerFactoryAsConstructor(vm, "TestEmailSendForm", forms.NewTestEmailSend)
845
+ registerFactoryAsConstructor(vm, "TestS3FilesystemForm", forms.NewTestS3Filesystem)
846
+ }
847
+
848
+ // BindApis registers $apis.* namespaced object with reusable Web API
849
+ // handlers, middlewares and other related helpers.
850
+ //
851
+ // See https://pocketbase.io/jsvm/modules/_apis.html.
852
+ func BindApis(vm *goja.Runtime) {
853
+ obj := vm.NewObject()
854
+ vm.Set("$apis", obj)
855
+
856
+ obj.Set("static", func(dirOrFS any, indexFallback bool) func(*core.RequestEvent) error {
857
+ switch v := dirOrFS.(type) {
858
+ case fs.FS:
859
+ return apis.Static(v, indexFallback)
860
+ case string:
861
+ return apis.Static(os.DirFS(v), indexFallback)
862
+ default:
863
+ panic("$apis.static expects the first argument to be either a plain string path or fs.FS value")
864
+ }
865
+ })
866
+
867
+ // middlewares
868
+ obj.Set("requireGuestOnly", apis.RequireGuestOnly)
869
+ obj.Set("requireAuth", apis.RequireAuth)
870
+ obj.Set("requireSuperuserAuth", apis.RequireSuperuserAuth)
871
+ obj.Set("requireSuperuserOrOwnerAuth", apis.RequireSuperuserOrOwnerAuth)
872
+ obj.Set("skipSuccessActivityLog", apis.SkipSuccessActivityLog)
873
+ obj.Set("gzip", apis.Gzip)
874
+ obj.Set("bodyLimit", apis.BodyLimit)
875
+
876
+ // record helpers
877
+ obj.Set("recordAuthResponse", apis.RecordAuthResponse)
878
+ obj.Set("enrichRecord", apis.EnrichRecord)
879
+ obj.Set("enrichRecords", apis.EnrichRecords)
880
+
881
+ // api errors
882
+ registerFactoryAsConstructor(vm, "ApiError", router.NewApiError)
883
+ registerFactoryAsConstructor(vm, "NotFoundError", router.NewNotFoundError)
884
+ registerFactoryAsConstructor(vm, "BadRequestError", router.NewBadRequestError)
885
+ registerFactoryAsConstructor(vm, "ForbiddenError", router.NewForbiddenError)
886
+ registerFactoryAsConstructor(vm, "UnauthorizedError", router.NewUnauthorizedError)
887
+ registerFactoryAsConstructor(vm, "TooManyRequestsError", router.NewTooManyRequestsError)
888
+ registerFactoryAsConstructor(vm, "InternalServerError", router.NewInternalServerError)
889
+ }
890
+
891
+ // BindHTTP registers $http.* namespaced object with common utils
892
+ // for sending HTTP requests.
893
+ //
894
+ // See https://pocketbase.io/jsvm/modules/_http.html.
895
+ func BindHTTP(vm *goja.Runtime) {
896
+ obj := vm.NewObject()
897
+ vm.Set("$http", obj)
898
+
899
+ vm.Set("FormData", func(call goja.ConstructorCall) *goja.Object {
900
+ instance := FormData{}
901
+
902
+ instanceValue := vm.ToValue(instance).(*goja.Object)
903
+ instanceValue.SetPrototype(call.This.Prototype())
904
+
905
+ return instanceValue
906
+ })
907
+
908
+ type sendResult struct {
909
+ JSON any `json:"json"`
910
+ Headers map[string][]string `json:"headers"`
911
+ Cookies map[string]*http.Cookie `json:"cookies"`
912
+
913
+ // Deprecated: consider using Body instead
914
+ Raw string `json:"raw"`
915
+
916
+ Body []byte `json:"body"`
917
+ StatusCode int `json:"statusCode"`
918
+ }
919
+
920
+ type sendConfig struct {
921
+ // Deprecated: consider using Body instead
922
+ Data map[string]any
923
+
924
+ Body any // raw string or FormData
925
+ Headers map[string]string
926
+ Method string
927
+ Url string
928
+ Timeout int // seconds (default to 120)
929
+ }
930
+
931
+ obj.Set("send", func(params map[string]any) (*sendResult, error) {
932
+ config := sendConfig{
933
+ Method: "GET",
934
+ }
935
+
936
+ if v, ok := params["data"]; ok {
937
+ config.Data = cast.ToStringMap(v)
938
+ }
939
+
940
+ if v, ok := params["body"]; ok {
941
+ config.Body = v
942
+ }
943
+
944
+ if v, ok := params["headers"]; ok {
945
+ config.Headers = cast.ToStringMapString(v)
946
+ }
947
+
948
+ if v, ok := params["method"]; ok {
949
+ config.Method = cast.ToString(v)
950
+ }
951
+
952
+ if v, ok := params["url"]; ok {
953
+ config.Url = cast.ToString(v)
954
+ }
955
+
956
+ if v, ok := params["timeout"]; ok {
957
+ config.Timeout = cast.ToInt(v)
958
+ }
959
+
960
+ if config.Timeout <= 0 {
961
+ config.Timeout = 120
962
+ }
963
+
964
+ ctx, cancel := context.WithTimeout(context.Background(), time.Duration(config.Timeout)*time.Second)
965
+ defer cancel()
966
+
967
+ var reqBody io.Reader
968
+ var contentType string
969
+
970
+ // legacy json body data
971
+ if len(config.Data) != 0 {
972
+ encoded, err := json.Marshal(config.Data)
973
+ if err != nil {
974
+ return nil, err
975
+ }
976
+ reqBody = bytes.NewReader(encoded)
977
+ } else {
978
+ switch v := config.Body.(type) {
979
+ case io.Reader:
980
+ reqBody = v
981
+ case FormData:
982
+ body, mp, err := v.toMultipart()
983
+ if err != nil {
984
+ return nil, err
985
+ }
986
+
987
+ reqBody = body
988
+ contentType = mp.FormDataContentType()
989
+ default:
990
+ reqBody = strings.NewReader(cast.ToString(config.Body))
991
+ }
992
+ }
993
+
994
+ req, err := http.NewRequestWithContext(ctx, strings.ToUpper(config.Method), config.Url, reqBody)
995
+ if err != nil {
996
+ return nil, err
997
+ }
998
+
999
+ for k, v := range config.Headers {
1000
+ req.Header.Add(k, v)
1001
+ }
1002
+
1003
+ // set the explicit content type
1004
+ // (overwriting the user provided header value if any)
1005
+ if contentType != "" {
1006
+ req.Header.Set("content-type", contentType)
1007
+ }
1008
+
1009
+ res, err := http.DefaultClient.Do(req)
1010
+ if err != nil {
1011
+ return nil, err
1012
+ }
1013
+ defer res.Body.Close()
1014
+
1015
+ bodyRaw, _ := io.ReadAll(res.Body)
1016
+
1017
+ result := &sendResult{
1018
+ StatusCode: res.StatusCode,
1019
+ Headers: map[string][]string{},
1020
+ Cookies: map[string]*http.Cookie{},
1021
+ Raw: string(bodyRaw),
1022
+ Body: bodyRaw,
1023
+ }
1024
+
1025
+ for k, v := range res.Header {
1026
+ result.Headers[k] = v
1027
+ }
1028
+
1029
+ for _, v := range res.Cookies() {
1030
+ result.Cookies[v.Name] = v
1031
+ }
1032
+
1033
+ if len(result.Body) > 0 {
1034
+ // try as map
1035
+ result.JSON = map[string]any{}
1036
+ if err := json.Unmarshal(bodyRaw, &result.JSON); err != nil {
1037
+ // try as slice
1038
+ result.JSON = []any{}
1039
+ if err := json.Unmarshal(bodyRaw, &result.JSON); err != nil {
1040
+ result.JSON = nil
1041
+ }
1042
+ }
1043
+ }
1044
+
1045
+ return result, nil
1046
+ })
1047
+ }
1048
+
1049
+ // -------------------------------------------------------------------
1050
+
1051
+ // checkGojaValueForError resolves the provided goja.Value and tries
1052
+ // to extract its underlying error value (if any).
1053
+ func checkGojaValueForError(app core.App, value goja.Value) error {
1054
+ if value == nil {
1055
+ return nil
1056
+ }
1057
+
1058
+ exported := value.Export()
1059
+ switch v := exported.(type) {
1060
+ case error:
1061
+ return v
1062
+ case *goja.Promise:
1063
+ // Promise as return result is not officially supported but try to
1064
+ // resolve any thrown exception to avoid silently ignoring it
1065
+ app.Logger().Warn("the handler must a non-async function and not return a Promise")
1066
+ if promiseErr, ok := v.Result().Export().(error); ok {
1067
+ return normalizeException(promiseErr)
1068
+ }
1069
+ }
1070
+
1071
+ return nil
1072
+ }
1073
+
1074
+ // normalizeException checks if the provided error is a goja.Exception
1075
+ // and attempts to return its underlying Go error.
1076
+ //
1077
+ // note: using just goja.Exception.Unwrap() is insufficient and may falsely result in nil.
1078
+ func normalizeException(err error) error {
1079
+ if err == nil {
1080
+ return nil
1081
+ }
1082
+
1083
+ jsException, ok := err.(*goja.Exception)
1084
+ if !ok {
1085
+ return err // no exception
1086
+ }
1087
+
1088
+ switch v := jsException.Value().Export().(type) {
1089
+ case error:
1090
+ err = v
1091
+ case map[string]any: // goja.GoError
1092
+ if vErr, ok := v["value"].(error); ok {
1093
+ err = vErr
1094
+ }
1095
+ }
1096
+
1097
+ return err
1098
+ }
1099
+
1100
+ var cachedFactoryFuncTypes = store.New[string, reflect.Type](nil)
1101
+
1102
+ // registerFactoryAsConstructor registers the factory function as native JS constructor.
1103
+ //
1104
+ // If there is missing or nil arguments, their type zero value is used.
1105
+ func registerFactoryAsConstructor(vm *goja.Runtime, constructorName string, factoryFunc any) {
1106
+ rv := reflect.ValueOf(factoryFunc)
1107
+ rt := cachedFactoryFuncTypes.GetOrSet(constructorName, func() reflect.Type {
1108
+ return reflect.TypeOf(factoryFunc)
1109
+ })
1110
+ totalArgs := rt.NumIn()
1111
+
1112
+ vm.Set(constructorName, func(call goja.ConstructorCall) *goja.Object {
1113
+ args := make([]reflect.Value, totalArgs)
1114
+
1115
+ for i := 0; i < totalArgs; i++ {
1116
+ v := call.Argument(i).Export()
1117
+
1118
+ // use the arg type zero value
1119
+ if v == nil {
1120
+ args[i] = reflect.New(rt.In(i)).Elem()
1121
+ } else if number, ok := v.(int64); ok {
1122
+ // goja uses int64 for "int"-like numbers but we rarely do that and use int most of the times
1123
+ // (at later stage we can use reflection on the arguments to validate the types in case this is not sufficient anymore)
1124
+ args[i] = reflect.ValueOf(int(number))
1125
+ } else {
1126
+ args[i] = reflect.ValueOf(v)
1127
+ }
1128
+ }
1129
+
1130
+ result := rv.Call(args)
1131
+
1132
+ if len(result) != 1 {
1133
+ panic("the factory function should return only 1 item")
1134
+ }
1135
+
1136
+ value := vm.ToValue(result[0].Interface()).(*goja.Object)
1137
+ value.SetPrototype(call.This.Prototype())
1138
+
1139
+ return value
1140
+ })
1141
+ }
1142
+
1143
+ // structConstructor wraps the provided struct with a native JS constructor.
1144
+ //
1145
+ // If the constructor argument is a map, each entry of the map will be loaded into the wrapped goja.Object.
1146
+ func structConstructor(vm *goja.Runtime, call goja.ConstructorCall, instance any) *goja.Object {
1147
+ data, _ := call.Argument(0).Export().(map[string]any)
1148
+
1149
+ instanceValue := vm.ToValue(instance).(*goja.Object)
1150
+ for k, v := range data {
1151
+ instanceValue.Set(k, v)
1152
+ }
1153
+
1154
+ instanceValue.SetPrototype(call.This.Prototype())
1155
+
1156
+ return instanceValue
1157
+ }
1158
+
1159
+ // structConstructorUnmarshal wraps the provided struct with a native JS constructor.
1160
+ //
1161
+ // The constructor first argument will be loaded via json.Unmarshal into the instance.
1162
+ func structConstructorUnmarshal(vm *goja.Runtime, call goja.ConstructorCall, instance any) *goja.Object {
1163
+ if data := call.Argument(0).Export(); data != nil {
1164
+ if raw, err := json.Marshal(data); err == nil {
1165
+ _ = json.Unmarshal(raw, instance)
1166
+ }
1167
+ }
1168
+
1169
+ instanceValue := vm.ToValue(instance).(*goja.Object)
1170
+ instanceValue.SetPrototype(call.This.Prototype())
1171
+
1172
+ return instanceValue
1173
+ }
1174
+
1175
+ var cachedDynamicModelStructs = store.New[string, reflect.Type](nil)
1176
+
1177
+ // newDynamicModel creates a new dynamic struct with fields based
1178
+ // on the specified "shape".
1179
+ //
1180
+ // The "shape" values are used as defaults and could be of type:
1181
+ //
1182
+ // - int64 (ex.: 0)
1183
+ // - *int64 (ex.: nullInt())
1184
+ // - float64 (ex.: -0)
1185
+ // - *float64 (ex.: nullFloat())
1186
+ // - string (ex.: "")
1187
+ // - *string (ex.: nullString())
1188
+ // - bool (ex.: false)
1189
+ // - *bool (ex.: nullBool())
1190
+ // - slice/arr (ex.: [])
1191
+ // - *slice/arr (ex.: nullArray())
1192
+ // - map (ex.: {})
1193
+ // - *map (ex.: nullObject())
1194
+ //
1195
+ // Example:
1196
+ //
1197
+ // m := newDynamicModel(map[string]any{
1198
+ // "title": "",
1199
+ // "total": 0,
1200
+ // })
1201
+ func newDynamicModel(shape map[string]any) any {
1202
+ info := make([]*shapeFieldInfo, 0, len(shape))
1203
+
1204
+ var hash strings.Builder
1205
+
1206
+ sortedKeys := make([]string, 0, len(shape))
1207
+ for k := range shape {
1208
+ sortedKeys = append(sortedKeys, k)
1209
+ }
1210
+ sort.Strings(sortedKeys)
1211
+
1212
+ for _, k := range sortedKeys {
1213
+ v := shape[k]
1214
+ vt := reflect.TypeOf(v)
1215
+
1216
+ switch vt.Kind() {
1217
+ case reflect.Map:
1218
+ raw, _ := json.Marshal(v)
1219
+ newV := types.JSONMap[any]{}
1220
+ _ = newV.Scan(raw)
1221
+ v = newV
1222
+ vt = reflect.TypeOf(v)
1223
+ case reflect.Slice, reflect.Array:
1224
+ raw, _ := json.Marshal(v)
1225
+ newV := types.JSONArray[any]{}
1226
+ _ = newV.Scan(raw)
1227
+ v = newV
1228
+ vt = reflect.TypeOf(newV)
1229
+ case reflect.Pointer:
1230
+ // for pointers always fallback to nil as their default value
1231
+ v = nil
1232
+ }
1233
+
1234
+ hash.WriteString(k)
1235
+ hash.WriteString(":")
1236
+ hash.WriteString(vt.String()) // it doesn't guarantee to be unique across all types but it should be fine with the primitive types DynamicModel is used
1237
+ hash.WriteString("|")
1238
+
1239
+ info = append(info, &shapeFieldInfo{key: k, value: v, valueType: vt})
1240
+ }
1241
+
1242
+ st := cachedDynamicModelStructs.GetOrSet(hash.String(), func() reflect.Type {
1243
+ structFields := make([]reflect.StructField, len(info))
1244
+
1245
+ for i, item := range info {
1246
+ structFields[i] = reflect.StructField{
1247
+ Name: inflector.UcFirst(item.key), // ensures that the field is exportable
1248
+ Type: item.valueType,
1249
+ Tag: reflect.StructTag(`db:"` + item.key + `" json:"` + item.key + `" form:"` + item.key + `"`),
1250
+ }
1251
+ }
1252
+
1253
+ return reflect.StructOf(structFields)
1254
+ })
1255
+
1256
+ elem := reflect.New(st).Elem()
1257
+
1258
+ // load default values into the new model
1259
+ for i, item := range info {
1260
+ if item.value == nil {
1261
+ continue
1262
+ }
1263
+ elem.Field(i).Set(reflect.ValueOf(item.value))
1264
+ }
1265
+
1266
+ return elem.Addr().Interface()
1267
+ }
1268
+
1269
+ type shapeFieldInfo struct {
1270
+ value any
1271
+ valueType reflect.Type
1272
+ key string
1273
+ }