@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.
- package/.dockerignore +9 -0
- package/Dockerfile +26 -0
- package/FEATURES.md +32 -0
- package/LICENSE.md +17 -0
- package/MIGRATION.md +49 -0
- package/NOTICE.md +5 -0
- package/README.md +26 -0
- package/adapter.go +31 -0
- package/admin/register.go +60 -0
- package/admin/register_test.go +37 -0
- package/backups/backup_encryption_test.go +82 -0
- package/backups/encryption.go +138 -0
- package/backups/integration_test.go +51 -0
- package/backups/register.go +120 -0
- package/backups/restore.go +119 -0
- package/backups/s3_test.go +52 -0
- package/backups/swap.go +53 -0
- package/backups/swap_test.go +75 -0
- package/backups/upload.go +52 -0
- package/bin/pocketbase-extension.mjs +25 -0
- package/cmd/edge/main.go +123 -0
- package/cmd/import-fork/main.go +37 -0
- package/cmd/loadtest/main.go +61 -0
- package/cmd/loadtest/sandbox.go +73 -0
- package/cmd/loadtest/sandbox_test.go +20 -0
- package/cmd/pocketbase/main.go +78 -0
- package/deploy/README.md +148 -0
- package/deploy/app/hooks/README.md +2 -0
- package/deploy/app/migrations/1789000000_notes.js +16 -0
- package/deploy/app/public/README.md +2 -0
- package/deploy/compose.secrets.yaml +8 -0
- package/deploy/compose.yaml +68 -0
- package/deploy/edge.json +13 -0
- package/edge/gateway.go +295 -0
- package/edge/gateway_test.go +296 -0
- package/edge/openapi.json +1 -0
- package/edge/policy.go +150 -0
- package/features/collection_singleton.go +13 -0
- package/features/collection_singleton_test.go +46 -0
- package/features/dimensions_test.go +65 -0
- package/features/duplicate.go +178 -0
- package/features/duplicate_test.go +128 -0
- package/features/field_color.go +46 -0
- package/features/field_date_only.go +39 -0
- package/features/field_json_schema.go +92 -0
- package/features/field_scalar_extensions_test.go +66 -0
- package/features/files.go +36 -0
- package/features/filter_has_any_test.go +81 -0
- package/features/generate_test.go +72 -0
- package/features/has_any_visibility_test.go +62 -0
- package/features/json.go +50 -0
- package/features/membership.go +64 -0
- package/features/register.go +45 -0
- package/features/schema_test.go +58 -0
- package/features/ui/main.js +133 -0
- package/features/ui/settings.js +31 -0
- package/go.mod +54 -0
- package/go.sum +159 -0
- package/internal/archive/create.go +91 -0
- package/internal/archive/create_test.go +125 -0
- package/internal/archive/extract.go +99 -0
- package/internal/archive/extract_test.go +88 -0
- package/jsvm/binds.go +1273 -0
- package/jsvm/binds_app_reset_test.go +314 -0
- package/jsvm/binds_test.go +1870 -0
- package/jsvm/form_data.go +149 -0
- package/jsvm/form_data_test.go +225 -0
- package/jsvm/internal/types/generated/embed.go +6 -0
- package/jsvm/internal/types/generated/types.d.ts +24820 -0
- package/jsvm/internal/types/types.go +1408 -0
- package/jsvm/jsvm.go +587 -0
- package/jsvm/mapper.go +67 -0
- package/jsvm/mapper_test.go +42 -0
- package/jsvm/pool.go +73 -0
- package/jsvm/program_source_test.go +24 -0
- package/loadtest/loadtest.go +202 -0
- package/loadtest/loadtest_test.go +84 -0
- package/localization/README.md +23 -0
- package/localization/catalogue.json +483 -0
- package/localization/localization.go +94 -0
- package/localization/localization_test.go +21 -0
- package/mail/register.go +80 -0
- package/mail/register_test.go +49 -0
- package/mail/resolve.go +91 -0
- package/migration/import.go +96 -0
- package/migration/import_test.go +58 -0
- package/otp/otp.go +56 -0
- package/otp/otp_test.go +56 -0
- package/package.json +51 -0
- package/scripts/check-edge.py +42 -0
- package/scripts/check.sh +11 -0
- package/scripts/sync-jsvm-types.sh +10 -0
- package/security/README.md +94 -0
- package/security/assurance_test.go +149 -0
- package/security/compatibility_test.go +128 -0
- package/security/config.go +78 -0
- package/security/dashboard_test.go +103 -0
- package/security/management.go +169 -0
- package/security/openapi.json +508 -0
- package/security/review.go +34 -0
- package/security/security.go +503 -0
- package/security/security_test.go +146 -0
- package/security/state.go +116 -0
- package/security/ui/dashboard.css +4 -0
- package/security/ui/dashboard.js +83 -0
- package/security/ui/main.js +15 -0
- package/security/ui/model.js +32 -0
- package/security/ui/model.test.mjs +25 -0
- package/security/ui/registration.test.mjs +10 -0
- package/settings/env_test.go +41 -0
- package/settings/openapi.json +193 -0
- package/settings/settings.go +155 -0
- package/settings/settings_test.go +31 -0
- package/watcher/watcher.go +192 -0
- package/watcher/watcher_test.go +200 -0
- package/web/static.go +99 -0
- package/web/static_test.go +48 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
package jsvm
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"io"
|
|
6
|
+
"mime/multipart"
|
|
7
|
+
|
|
8
|
+
"github.com/pocketbase/pocketbase/tools/filesystem"
|
|
9
|
+
"github.com/spf13/cast"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// FormData represents an interface similar to the browser's [FormData].
|
|
13
|
+
//
|
|
14
|
+
// The value of each FormData entry must be a string or [*filesystem.File] instance.
|
|
15
|
+
//
|
|
16
|
+
// It is intended to be used together with the JSVM `$http.send` when
|
|
17
|
+
// sending multipart/form-data requests.
|
|
18
|
+
//
|
|
19
|
+
// [FormData]: https://developer.mozilla.org/en-US/docs/Web/API/FormData.
|
|
20
|
+
type FormData map[string][]any
|
|
21
|
+
|
|
22
|
+
// Append appends a new value onto an existing key inside the current FormData,
|
|
23
|
+
// or adds the key if it does not already exist.
|
|
24
|
+
func (data FormData) Append(key string, value any) {
|
|
25
|
+
data[key] = append(data[key], value)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Set sets a new value for an existing key inside the current FormData,
|
|
29
|
+
// or adds the key/value if it does not already exist.
|
|
30
|
+
func (data FormData) Set(key string, value any) {
|
|
31
|
+
data[key] = []any{value}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Delete deletes a key and its value(s) from the current FormData.
|
|
35
|
+
func (data FormData) Delete(key string) {
|
|
36
|
+
delete(data, key)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get returns the first value associated with a given key from
|
|
40
|
+
// within the current FormData.
|
|
41
|
+
//
|
|
42
|
+
// If you expect multiple values and want all of them,
|
|
43
|
+
// use the [FormData.GetAll] method instead.
|
|
44
|
+
func (data FormData) Get(key string) any {
|
|
45
|
+
values, ok := data[key]
|
|
46
|
+
if !ok || len(values) == 0 {
|
|
47
|
+
return nil
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return values[0]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// GetAll returns all the values associated with a given key
|
|
54
|
+
// from within the current FormData.
|
|
55
|
+
func (data FormData) GetAll(key string) []any {
|
|
56
|
+
values, ok := data[key]
|
|
57
|
+
if !ok {
|
|
58
|
+
return nil
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return values
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Has returns whether a FormData object contains a certain key.
|
|
65
|
+
func (data FormData) Has(key string) bool {
|
|
66
|
+
values, ok := data[key]
|
|
67
|
+
|
|
68
|
+
return ok && len(values) > 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Keys returns all keys contained in the current FormData.
|
|
72
|
+
func (data FormData) Keys() []string {
|
|
73
|
+
result := make([]string, 0, len(data))
|
|
74
|
+
|
|
75
|
+
for k := range data {
|
|
76
|
+
result = append(result, k)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Keys returns all values contained in the current FormData.
|
|
83
|
+
func (data FormData) Values() []any {
|
|
84
|
+
result := make([]any, 0, len(data))
|
|
85
|
+
|
|
86
|
+
for _, values := range data {
|
|
87
|
+
result = append(result, values...)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Entries returns a [key, value] slice pair for each FormData entry.
|
|
94
|
+
func (data FormData) Entries() [][]any {
|
|
95
|
+
result := make([][]any, 0, len(data))
|
|
96
|
+
|
|
97
|
+
for k, values := range data {
|
|
98
|
+
for _, v := range values {
|
|
99
|
+
result = append(result, []any{k, v})
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return result
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// toMultipart converts the current FormData entries into multipart encoded data.
|
|
107
|
+
func (data FormData) toMultipart() (*bytes.Buffer, *multipart.Writer, error) {
|
|
108
|
+
body := new(bytes.Buffer)
|
|
109
|
+
|
|
110
|
+
mp := multipart.NewWriter(body)
|
|
111
|
+
defer mp.Close()
|
|
112
|
+
|
|
113
|
+
for k, values := range data {
|
|
114
|
+
for _, rawValue := range values {
|
|
115
|
+
switch v := rawValue.(type) {
|
|
116
|
+
case *filesystem.File:
|
|
117
|
+
err := func() error {
|
|
118
|
+
mpw, err := mp.CreateFormFile(k, v.OriginalName)
|
|
119
|
+
if err != nil {
|
|
120
|
+
return err
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
file, err := v.Reader.Open()
|
|
124
|
+
if err != nil {
|
|
125
|
+
return err
|
|
126
|
+
}
|
|
127
|
+
defer file.Close()
|
|
128
|
+
|
|
129
|
+
_, err = io.Copy(mpw, file)
|
|
130
|
+
if err != nil {
|
|
131
|
+
return err
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return nil
|
|
135
|
+
}()
|
|
136
|
+
if err != nil {
|
|
137
|
+
return nil, nil, err
|
|
138
|
+
}
|
|
139
|
+
default:
|
|
140
|
+
err := mp.WriteField(k, cast.ToString(v))
|
|
141
|
+
if err != nil {
|
|
142
|
+
return nil, nil, err
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return body, mp, nil
|
|
149
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
package jsvm
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"encoding/json/v2"
|
|
6
|
+
"strings"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/pocketbase/pocketbase/tools/filesystem"
|
|
10
|
+
"github.com/pocketbase/pocketbase/tools/list"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func TestFormDataAppendAndSet(t *testing.T) {
|
|
14
|
+
t.Parallel()
|
|
15
|
+
|
|
16
|
+
data := FormData{}
|
|
17
|
+
|
|
18
|
+
data.Append("a", 1)
|
|
19
|
+
data.Append("a", 2)
|
|
20
|
+
|
|
21
|
+
data.Append("b", 3)
|
|
22
|
+
data.Append("b", 4)
|
|
23
|
+
data.Set("b", 5) // should overwrite the previous 2 calls
|
|
24
|
+
|
|
25
|
+
data.Set("c", 6)
|
|
26
|
+
data.Set("c", 7)
|
|
27
|
+
|
|
28
|
+
if len(data["a"]) != 2 {
|
|
29
|
+
t.Fatalf("Expected 2 'a' values, got %v", data["a"])
|
|
30
|
+
}
|
|
31
|
+
if data["a"][0] != 1 || data["a"][1] != 2 {
|
|
32
|
+
t.Fatalf("Expected 1 and 2 'a' key values, got %v", data["a"])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if len(data["b"]) != 1 {
|
|
36
|
+
t.Fatalf("Expected 1 'b' values, got %v", data["b"])
|
|
37
|
+
}
|
|
38
|
+
if data["b"][0] != 5 {
|
|
39
|
+
t.Fatalf("Expected 5 as 'b' key value, got %v", data["b"])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if len(data["c"]) != 1 {
|
|
43
|
+
t.Fatalf("Expected 1 'c' values, got %v", data["c"])
|
|
44
|
+
}
|
|
45
|
+
if data["c"][0] != 7 {
|
|
46
|
+
t.Fatalf("Expected 7 as 'c' key value, got %v", data["c"])
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
func TestFormDataDelete(t *testing.T) {
|
|
51
|
+
t.Parallel()
|
|
52
|
+
|
|
53
|
+
data := FormData{}
|
|
54
|
+
data.Append("a", 1)
|
|
55
|
+
data.Append("a", 2)
|
|
56
|
+
data.Append("b", 3)
|
|
57
|
+
|
|
58
|
+
data.Delete("missing") // should do nothing
|
|
59
|
+
data.Delete("a")
|
|
60
|
+
|
|
61
|
+
if len(data) != 1 {
|
|
62
|
+
t.Fatalf("Expected exactly 1 data remaining key, got %v", data)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if data["b"][0] != 3 {
|
|
66
|
+
t.Fatalf("Expected 3 as 'b' key value, got %v", data["b"])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
func TestFormDataGet(t *testing.T) {
|
|
71
|
+
t.Parallel()
|
|
72
|
+
|
|
73
|
+
data := FormData{}
|
|
74
|
+
data.Append("a", 1)
|
|
75
|
+
data.Append("a", 2)
|
|
76
|
+
|
|
77
|
+
if v := data.Get("missing"); v != nil {
|
|
78
|
+
t.Fatalf("Expected %v for key 'missing', got %v", nil, v)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if v := data.Get("a"); v != 1 {
|
|
82
|
+
t.Fatalf("Expected %v for key 'a', got %v", 1, v)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func TestFormDataGetAll(t *testing.T) {
|
|
87
|
+
t.Parallel()
|
|
88
|
+
|
|
89
|
+
data := FormData{}
|
|
90
|
+
data.Append("a", 1)
|
|
91
|
+
data.Append("a", 2)
|
|
92
|
+
|
|
93
|
+
if v := data.GetAll("missing"); v != nil {
|
|
94
|
+
t.Fatalf("Expected %v for key 'a', got %v", nil, v)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
values := data.GetAll("a")
|
|
98
|
+
if len(values) != 2 || values[0] != 1 || values[1] != 2 {
|
|
99
|
+
t.Fatalf("Expected 1 and 2 values, got %v", values)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func TestFormDataHas(t *testing.T) {
|
|
104
|
+
t.Parallel()
|
|
105
|
+
|
|
106
|
+
data := FormData{}
|
|
107
|
+
data.Append("a", 1)
|
|
108
|
+
|
|
109
|
+
if v := data.Has("missing"); v {
|
|
110
|
+
t.Fatalf("Expected key 'missing' to not exist: %v", v)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if v := data.Has("a"); !v {
|
|
114
|
+
t.Fatalf("Expected key 'a' to exist: %v", v)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func TestFormDataKeys(t *testing.T) {
|
|
119
|
+
t.Parallel()
|
|
120
|
+
|
|
121
|
+
data := FormData{}
|
|
122
|
+
data.Append("a", 1)
|
|
123
|
+
data.Append("b", 1)
|
|
124
|
+
data.Append("c", 1)
|
|
125
|
+
data.Append("a", 1)
|
|
126
|
+
|
|
127
|
+
keys := data.Keys()
|
|
128
|
+
|
|
129
|
+
expectedKeys := []string{"a", "b", "c"}
|
|
130
|
+
|
|
131
|
+
for _, expected := range expectedKeys {
|
|
132
|
+
if !list.ExistInSlice(expected, keys) {
|
|
133
|
+
t.Fatalf("Expected key %s to exists in %v", expected, keys)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
func TestFormDataValues(t *testing.T) {
|
|
139
|
+
t.Parallel()
|
|
140
|
+
|
|
141
|
+
data := FormData{}
|
|
142
|
+
data.Append("a", 1)
|
|
143
|
+
data.Append("b", 2)
|
|
144
|
+
data.Append("c", 3)
|
|
145
|
+
data.Append("a", 4)
|
|
146
|
+
|
|
147
|
+
values := data.Values()
|
|
148
|
+
|
|
149
|
+
expectedKeys := []any{1, 2, 3, 4}
|
|
150
|
+
|
|
151
|
+
for _, expected := range expectedKeys {
|
|
152
|
+
if !list.ExistInSlice(expected, values) {
|
|
153
|
+
t.Fatalf("Expected key %s to exists in %v", expected, values)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func TestFormDataEntries(t *testing.T) {
|
|
159
|
+
t.Parallel()
|
|
160
|
+
|
|
161
|
+
data := FormData{}
|
|
162
|
+
data.Append("a", 1)
|
|
163
|
+
data.Append("b", 2)
|
|
164
|
+
data.Append("c", 3)
|
|
165
|
+
data.Append("a", 4)
|
|
166
|
+
|
|
167
|
+
entries := data.Entries()
|
|
168
|
+
|
|
169
|
+
rawEntries, err := json.Marshal(entries, json.Deterministic(true))
|
|
170
|
+
if err != nil {
|
|
171
|
+
t.Fatal(err)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if len(entries) != 4 {
|
|
175
|
+
t.Fatalf("Expected 4 entries")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
expectedEntries := []string{`["a",1]`, `["a",4]`, `["b",2]`, `["c",3]`}
|
|
179
|
+
for _, expected := range expectedEntries {
|
|
180
|
+
if !bytes.Contains(rawEntries, []byte(expected)) {
|
|
181
|
+
t.Fatalf("Expected entry %s to exists in %s", expected, rawEntries)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
func TestFormDataToMultipart(t *testing.T) {
|
|
187
|
+
t.Parallel()
|
|
188
|
+
|
|
189
|
+
f, err := filesystem.NewFileFromBytes([]byte("abc"), "test")
|
|
190
|
+
if err != nil {
|
|
191
|
+
t.Fatal(err)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
data := FormData{}
|
|
195
|
+
data.Append("a", 1) // should be casted
|
|
196
|
+
data.Append("b", "test1")
|
|
197
|
+
data.Append("b", "test2")
|
|
198
|
+
data.Append("c", f)
|
|
199
|
+
|
|
200
|
+
body, mp, err := data.toMultipart()
|
|
201
|
+
if err != nil {
|
|
202
|
+
t.Fatal(err)
|
|
203
|
+
}
|
|
204
|
+
bodyStr := body.String()
|
|
205
|
+
|
|
206
|
+
// content type checks
|
|
207
|
+
contentType := mp.FormDataContentType()
|
|
208
|
+
expectedContentType := "multipart/form-data; boundary="
|
|
209
|
+
if !strings.Contains(contentType, expectedContentType) {
|
|
210
|
+
t.Fatalf("Expected to find content-type %s in %s", expectedContentType, contentType)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// body checks
|
|
214
|
+
expectedBodyParts := []string{
|
|
215
|
+
"Content-Disposition: form-data; name=\"a\"\r\n\r\n1",
|
|
216
|
+
"Content-Disposition: form-data; name=\"b\"\r\n\r\ntest1",
|
|
217
|
+
"Content-Disposition: form-data; name=\"b\"\r\n\r\ntest2",
|
|
218
|
+
"Content-Disposition: form-data; name=\"c\"; filename=\"test\"\r\nContent-Type: application/octet-stream\r\n\r\nabc",
|
|
219
|
+
}
|
|
220
|
+
for _, part := range expectedBodyParts {
|
|
221
|
+
if !strings.Contains(bodyStr, part) {
|
|
222
|
+
t.Fatalf("Expected to find %s in body\n%s", part, bodyStr)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|